From be55b2499bd7f3ddd266c01e64ce7b66ae0c78a0 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 14:31:25 +0000 Subject: [PATCH 01/60] chore(node): upgrade to core v0.8.0-alpha.1 Co-Authored-By: Virgil --- CLAUDE.md | 6 ++-- go.mod | 5 ++-- go.sum | 66 +++++++++++++++++++++++++++++++++++++++---- logging/logger.go | 4 +-- node/bundle.go | 57 ++++++++++++++++++------------------- node/controller.go | 30 ++++++++++---------- node/core_fs.go | 53 ++++++++++++++++++++++++++++++++++ node/dispatcher.go | 8 +++--- node/errors.go | 6 ++-- node/identity.go | 63 ++++++++++++++++++++--------------------- node/levin/header.go | 6 ++-- node/levin/storage.go | 26 ++++++++--------- node/levin/varint.go | 24 ++++++++-------- node/peer.go | 45 +++++++++++++++-------------- node/protocol.go | 8 +++--- node/transport.go | 36 +++++++++++------------ node/worker.go | 31 ++++++++++---------- ueps/packet.go | 13 ++++----- ueps/reader.go | 7 ++--- 19 files changed, 297 insertions(+), 197 deletions(-) create mode 100644 node/core_fs.go diff --git a/CLAUDE.md b/CLAUDE.md index 5f35b3c..27d11bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -`go-p2p` is the P2P networking layer for the Lethean network. Module path: `forge.lthn.ai/core/go-p2p` +`go-p2p` is the P2P networking layer for the Lethean network. Module path: `dappco.re/go/core/p2p` ## Prerequisites @@ -80,8 +80,8 @@ type ProfileManager interface { - Licence: EUPL-1.2 — new files need `// SPDX-License-Identifier: EUPL-1.2` - Security-first: do not weaken HMAC, challenge-response, Zip Slip defence, or rate limiting - Use `logging` package only — no `fmt.Println` or `log.Printf` in library code -- Error handling: use `coreerr.E()` from `go-log` — never `fmt.Errorf` or `errors.New` in library code -- File I/O: use `coreio.Local` from `go-io` — never `os.ReadFile`/`os.WriteFile` in library code (exception: `os.OpenFile` for streaming writes where `coreio` lacks support) +- Error handling: use `core.E()` from `dappco.re/go/core` — never `fmt.Errorf` or `errors.New` in library code +- File I/O: use `dappco.re/go/core` filesystem helpers (package-level adapters in `node/` backed by `core.Fs`) — never `os.ReadFile`/`os.WriteFile` in library code (exception: `os.OpenFile` for streaming writes where filesystem helpers cannot preserve tar header mode bits) - Hot-path debug logging uses sampling pattern: `if counter.Add(1)%interval == 0` ### Transport test helper diff --git a/go.mod b/go.mod index 003f271..e395272 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module dappco.re/go/core/p2p go 1.26.0 require ( - dappco.re/go/core/io v0.2.0 - dappco.re/go/core/log v0.1.0 + dappco.re/go/core v0.8.0-alpha.1 forge.lthn.ai/Snider/Borg v0.3.1 forge.lthn.ai/Snider/Poindexter v0.0.3 github.com/adrg/xdg v0.5.3 @@ -15,11 +14,11 @@ require ( require ( forge.lthn.ai/Snider/Enchantrix v0.0.4 // indirect - forge.lthn.ai/core/go-log v0.0.4 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/klauspost/compress v1.18.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/sys v0.42.0 // indirect diff --git a/go.sum b/go.sum index d7916fc..e06c31a 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,99 @@ -dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= -dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= -dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= -dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= forge.lthn.ai/Snider/Enchantrix v0.0.4/go.mod h1:OGCwuVeZPq3OPe2h6TX/ZbgEjHU6B7owpIBeXQGbSe0= forge.lthn.ai/Snider/Poindexter v0.0.3 h1:cx5wRhuLRKBM8riIZyNVAT2a8rwRhn1dodFBktocsVE= forge.lthn.ai/Snider/Poindexter v0.0.3/go.mod h1:ddzGia98k3HKkR0gl58IDzqz+MmgW2cQJOCNLfuWPpo= -forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= -forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging/logger.go b/logging/logger.go index 4ab0666..d368b82 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -10,7 +10,7 @@ import ( "sync" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // Level represents the severity of a log message. @@ -280,6 +280,6 @@ func ParseLevel(s string) (Level, error) { case "ERROR": return LevelError, nil default: - return LevelInfo, coreerr.E("logging.ParseLevel", "unknown log level: "+s, nil) + return LevelInfo, core.E("logging.ParseLevel", "unknown log level: "+s, nil) } } diff --git a/node/bundle.go b/node/bundle.go index 8c48f57..e60a659 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -11,8 +11,7 @@ import ( "path/filepath" "strings" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "forge.lthn.ai/Snider/Borg/pkg/datanode" "forge.lthn.ai/Snider/Borg/pkg/tim" @@ -50,14 +49,14 @@ func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bun // Create a TIM with just the profile config t, err := tim.New() if err != nil { - return nil, coreerr.E("CreateProfileBundle", "failed to create TIM", err) + return nil, core.E("CreateProfileBundle", "failed to create TIM", err) } t.Config = profileJSON // Encrypt to STIM format stimData, err := t.ToSigil(password) if err != nil { - return nil, coreerr.E("CreateProfileBundle", "failed to encrypt bundle", err) + return nil, core.E("CreateProfileBundle", "failed to encrypt bundle", err) } // Calculate checksum @@ -86,9 +85,9 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e // CreateMinerBundle creates an encrypted bundle containing a miner binary and optional profile. func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary - minerContent, err := coreio.Local.Read(minerPath) + minerContent, err := fsRead(minerPath) if err != nil { - return nil, coreerr.E("CreateMinerBundle", "failed to read miner binary", err) + return nil, core.E("CreateMinerBundle", "failed to read miner binary", err) } minerData := []byte(minerContent) @@ -97,19 +96,19 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo filepath.Base(minerPath): minerData, }) if err != nil { - return nil, coreerr.E("CreateMinerBundle", "failed to create tarball", err) + return nil, core.E("CreateMinerBundle", "failed to create tarball", err) } // Create DataNode from tarball dn, err := datanode.FromTar(tarData) if err != nil { - return nil, coreerr.E("CreateMinerBundle", "failed to create datanode", err) + return nil, core.E("CreateMinerBundle", "failed to create datanode", err) } // Create TIM from DataNode t, err := tim.FromDataNode(dn) if err != nil { - return nil, coreerr.E("CreateMinerBundle", "failed to create TIM", err) + return nil, core.E("CreateMinerBundle", "failed to create TIM", err) } // Set profile as config if provided @@ -120,7 +119,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo // Encrypt to STIM format stimData, err := t.ToSigil(password) if err != nil { - return nil, coreerr.E("CreateMinerBundle", "failed to encrypt bundle", err) + return nil, core.E("CreateMinerBundle", "failed to encrypt bundle", err) } checksum := calculateChecksum(stimData) @@ -137,7 +136,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { - return nil, coreerr.E("ExtractProfileBundle", "checksum mismatch - bundle may be corrupted", nil) + return nil, core.E("ExtractProfileBundle", "checksum mismatch - bundle may be corrupted", nil) } // If it's unencrypted JSON, just return it @@ -148,7 +147,7 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Decrypt STIM format t, err := tim.FromSigil(bundle.Data, password) if err != nil { - return nil, coreerr.E("ExtractProfileBundle", "failed to decrypt bundle", err) + return nil, core.E("ExtractProfileBundle", "failed to decrypt bundle", err) } return t.Config, nil @@ -158,25 +157,25 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { - return "", nil, coreerr.E("ExtractMinerBundle", "checksum mismatch - bundle may be corrupted", nil) + return "", nil, core.E("ExtractMinerBundle", "checksum mismatch - bundle may be corrupted", nil) } // Decrypt STIM format t, err := tim.FromSigil(bundle.Data, password) if err != nil { - return "", nil, coreerr.E("ExtractMinerBundle", "failed to decrypt bundle", err) + return "", nil, core.E("ExtractMinerBundle", "failed to decrypt bundle", err) } // Convert rootfs to tarball and extract tarData, err := t.RootFS.ToTar() if err != nil { - return "", nil, coreerr.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) + return "", nil, core.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) } // Extract tarball to destination minerPath, err := extractTarball(tarData, destDir) if err != nil { - return "", nil, coreerr.E("ExtractMinerBundle", "failed to extract tarball", err) + return "", nil, core.E("ExtractMinerBundle", "failed to extract tarball", err) } return minerPath, t.Config, nil @@ -256,11 +255,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir, err := filepath.Abs(destDir) if err != nil { - return "", coreerr.E("extractTarball", "failed to resolve destination directory", err) + return "", core.E("extractTarball", "failed to resolve destination directory", err) } absDestDir = filepath.Clean(absDestDir) - if err := coreio.Local.EnsureDir(absDestDir); err != nil { + if err := fsEnsureDir(absDestDir); err != nil { return "", err } @@ -281,12 +280,12 @@ func extractTarball(tarData []byte, destDir string) (string, error) { // Reject absolute paths if filepath.IsAbs(cleanName) { - return "", coreerr.E("extractTarball", "invalid tar entry: absolute path not allowed: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: absolute path not allowed: "+hdr.Name, nil) } // Reject paths that escape the destination directory if strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) || cleanName == ".." { - return "", coreerr.E("extractTarball", "invalid tar entry: path traversal attempt: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: path traversal attempt: "+hdr.Name, nil) } // Build the full path and verify it's within destDir @@ -295,26 +294,26 @@ func extractTarball(tarData []byte, destDir string) (string, error) { // Final security check: ensure the path is still within destDir if !strings.HasPrefix(fullPath, absDestDir+string(os.PathSeparator)) && fullPath != absDestDir { - return "", coreerr.E("extractTarball", "invalid tar entry: path escape attempt: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: path escape attempt: "+hdr.Name, nil) } switch hdr.Typeflag { case tar.TypeDir: - if err := coreio.Local.EnsureDir(fullPath); err != nil { + if err := fsEnsureDir(fullPath); err != nil { return "", err } case tar.TypeReg: // Ensure parent directory exists - if err := coreio.Local.EnsureDir(filepath.Dir(fullPath)); err != nil { + if err := fsEnsureDir(filepath.Dir(fullPath)); err != nil { return "", err } - // os.OpenFile is used deliberately here instead of coreio.Local.Create/Write - // because coreio hardcodes file permissions (0644) and we need to preserve + // os.OpenFile is used deliberately here instead of core.Fs.Create/Write + // because the helper writes with fixed default permissions and we need to preserve // the tar header's mode bits — executable binaries require 0755. f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)) if err != nil { - return "", coreerr.E("extractTarball", "failed to create file "+hdr.Name, err) + return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) } // Limit file size to prevent decompression bombs (100MB max per file) @@ -323,11 +322,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) { written, err := io.Copy(f, limitedReader) f.Close() if err != nil { - return "", coreerr.E("extractTarball", "failed to write file "+hdr.Name, err) + return "", core.E("extractTarball", "failed to write file "+hdr.Name, err) } if written > maxFileSize { - coreio.Local.Delete(fullPath) - return "", coreerr.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) + fsDelete(fullPath) + return "", core.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) } // Track first executable diff --git a/node/controller.go b/node/controller.go index 224c4d6..a79bbfc 100644 --- a/node/controller.go +++ b/node/controller.go @@ -6,7 +6,7 @@ import ( "sync" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/p2p/logging" ) @@ -67,11 +67,11 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat if c.transport.GetConnection(peerID) == nil { peer := c.peers.GetPeer(peerID) if peer == nil { - return nil, coreerr.E("Controller.sendRequest", "peer not found: "+peerID, nil) + return nil, core.E("Controller.sendRequest", "peer not found: "+peerID, nil) } conn, err := c.transport.Connect(peer) if err != nil { - return nil, coreerr.E("Controller.sendRequest", "failed to connect to peer", err) + return nil, core.E("Controller.sendRequest", "failed to connect to peer", err) } // Use the real peer ID after handshake (it may have changed) actualPeerID = conn.Peer.ID @@ -96,7 +96,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat // Send the message if err := c.transport.Send(actualPeerID, msg); err != nil { - return nil, coreerr.E("Controller.sendRequest", "failed to send message", err) + return nil, core.E("Controller.sendRequest", "failed to send message", err) } // Wait for response @@ -107,7 +107,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat case resp := <-respCh: return resp, nil case <-ctx.Done(): - return nil, coreerr.E("Controller.sendRequest", "request timeout", nil) + return nil, core.E("Controller.sendRequest", "request timeout", nil) } } @@ -120,7 +120,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) if err != nil { - return nil, coreerr.E("Controller.GetRemoteStats", "failed to create message", err) + return nil, core.E("Controller.GetRemoteStats", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -144,7 +144,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi } if minerType == "" { - return coreerr.E("Controller.StartRemoteMiner", "miner type is required", nil) + return core.E("Controller.StartRemoteMiner", "miner type is required", nil) } payload := StartMinerPayload{ @@ -155,7 +155,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi msg, err := NewMessage(MsgStartMiner, identity.ID, peerID, payload) if err != nil { - return coreerr.E("Controller.StartRemoteMiner", "failed to create message", err) + return core.E("Controller.StartRemoteMiner", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 30*time.Second) @@ -169,7 +169,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi } if !ack.Success { - return coreerr.E("Controller.StartRemoteMiner", "miner start failed: "+ack.Error, nil) + return core.E("Controller.StartRemoteMiner", "miner start failed: "+ack.Error, nil) } return nil @@ -188,7 +188,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { msg, err := NewMessage(MsgStopMiner, identity.ID, peerID, payload) if err != nil { - return coreerr.E("Controller.StopRemoteMiner", "failed to create message", err) + return core.E("Controller.StopRemoteMiner", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 30*time.Second) @@ -202,7 +202,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { } if !ack.Success { - return coreerr.E("Controller.StopRemoteMiner", "miner stop failed: "+ack.Error, nil) + return core.E("Controller.StopRemoteMiner", "miner stop failed: "+ack.Error, nil) } return nil @@ -222,7 +222,7 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin msg, err := NewMessage(MsgGetLogs, identity.ID, peerID, payload) if err != nil { - return nil, coreerr.E("Controller.GetRemoteLogs", "failed to create message", err) + return nil, core.E("Controller.GetRemoteLogs", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -281,7 +281,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { msg, err := NewMessage(MsgPing, identity.ID, peerID, payload) if err != nil { - return 0, coreerr.E("Controller.PingPeer", "failed to create message", err) + return 0, core.E("Controller.PingPeer", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 5*time.Second) @@ -309,7 +309,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { func (c *Controller) ConnectToPeer(peerID string) error { peer := c.peers.GetPeer(peerID) if peer == nil { - return coreerr.E("Controller.ConnectToPeer", "peer not found: "+peerID, nil) + return core.E("Controller.ConnectToPeer", "peer not found: "+peerID, nil) } _, err := c.transport.Connect(peer) @@ -320,7 +320,7 @@ func (c *Controller) ConnectToPeer(peerID string) error { func (c *Controller) DisconnectFromPeer(peerID string) error { conn := c.transport.GetConnection(peerID) if conn == nil { - return coreerr.E("Controller.DisconnectFromPeer", "peer not connected: "+peerID, nil) + return core.E("Controller.DisconnectFromPeer", "peer not connected: "+peerID, nil) } return conn.Close() diff --git a/node/core_fs.go b/node/core_fs.go new file mode 100644 index 0000000..f7056c8 --- /dev/null +++ b/node/core_fs.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package node + +import core "dappco.re/go/core" + +var localFS = (&core.Fs{}).New("/") + +func fsEnsureDir(path string) error { + return fsResultErr(localFS.EnsureDir(path)) +} + +func fsWrite(path, content string) error { + return fsResultErr(localFS.Write(path, content)) +} + +func fsRead(path string) (string, error) { + result := localFS.Read(path) + if !result.OK { + return "", fsResultErr(result) + } + + content, ok := result.Value.(string) + if !ok { + return "", core.E("node.fsRead", "filesystem read returned non-string content", nil) + } + + return content, nil +} + +func fsDelete(path string) error { + return fsResultErr(localFS.Delete(path)) +} + +func fsRename(oldPath, newPath string) error { + return fsResultErr(localFS.Rename(oldPath, newPath)) +} + +func fsExists(path string) bool { + return localFS.Exists(path) +} + +func fsResultErr(result core.Result) error { + if result.OK { + return nil + } + + if err, ok := result.Value.(error); ok && err != nil { + return err + } + + return core.E("node.fs", "filesystem operation failed", nil) +} diff --git a/node/dispatcher.go b/node/dispatcher.go index c240e18..a91a0ac 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -5,7 +5,7 @@ import ( "iter" "sync" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/p2p/logging" "dappco.re/go/core/p2p/ueps" @@ -134,12 +134,12 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { var ( // ErrThreatScoreExceeded is returned when a packet's ThreatScore exceeds // the safety threshold. - ErrThreatScoreExceeded = coreerr.E("Dispatcher.Dispatch", fmt.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) + ErrThreatScoreExceeded = core.E("Dispatcher.Dispatch", fmt.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) // ErrUnknownIntent is returned when no handler is registered for the // packet's IntentID. - ErrUnknownIntent = coreerr.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) + ErrUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) // ErrNilPacket is returned when a nil packet is passed to Dispatch. - ErrNilPacket = coreerr.E("Dispatcher.Dispatch", "nil packet", nil) + ErrNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) diff --git a/node/errors.go b/node/errors.go index 218610b..96cf152 100644 --- a/node/errors.go +++ b/node/errors.go @@ -1,14 +1,14 @@ package node -import coreerr "dappco.re/go/core/log" +import core "dappco.re/go/core" // Sentinel errors shared across the node package. var ( // ErrIdentityNotInitialized is returned when a node operation requires // a node identity but none has been generated or loaded. - ErrIdentityNotInitialized = coreerr.E("node", "node identity not initialized", nil) + ErrIdentityNotInitialized = core.E("node", "node identity not initialized", nil) // ErrMinerManagerNotConfigured is returned when a miner operation is // attempted but no MinerManager has been set on the Worker. - ErrMinerManagerNotConfigured = coreerr.E("node", "miner manager not configured", nil) + ErrMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) diff --git a/node/identity.go b/node/identity.go index 5b650a5..d745c5c 100644 --- a/node/identity.go +++ b/node/identity.go @@ -12,8 +12,7 @@ import ( "sync" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "forge.lthn.ai/Snider/Borg/pkg/stmf" "github.com/adrg/xdg" @@ -26,7 +25,7 @@ const ChallengeSize = 32 func GenerateChallenge() ([]byte, error) { challenge := make([]byte, ChallengeSize) if _, err := rand.Read(challenge); err != nil { - return nil, coreerr.E("GenerateChallenge", "failed to generate challenge", err) + return nil, core.E("GenerateChallenge", "failed to generate challenge", err) } return challenge, nil } @@ -80,12 +79,12 @@ type NodeManager struct { func NewNodeManager() (*NodeManager, error) { keyPath, err := xdg.DataFile("lethean-desktop/node/private.key") if err != nil { - return nil, coreerr.E("NodeManager.New", "failed to get key path", err) + return nil, core.E("NodeManager.New", "failed to get key path", err) } configPath, err := xdg.ConfigFile("lethean-desktop/node.json") if err != nil { - return nil, coreerr.E("NodeManager.New", "failed to get config path", err) + return nil, core.E("NodeManager.New", "failed to get config path", err) } return NewNodeManagerWithPaths(keyPath, configPath) @@ -135,7 +134,7 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { // Generate X25519 keypair using STMF keyPair, err := stmf.GenerateKeyPair() if err != nil { - return coreerr.E("NodeManager.GenerateIdentity", "failed to generate keypair", err) + return core.E("NodeManager.GenerateIdentity", "failed to generate keypair", err) } // Derive node ID from public key (first 16 bytes as hex = 32 char ID) @@ -156,12 +155,12 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { // Save private key if err := n.savePrivateKey(); err != nil { - return coreerr.E("NodeManager.GenerateIdentity", "failed to save private key", err) + return core.E("NodeManager.GenerateIdentity", "failed to save private key", err) } // Save identity config if err := n.saveIdentity(); err != nil { - return coreerr.E("NodeManager.GenerateIdentity", "failed to save identity", err) + return core.E("NodeManager.GenerateIdentity", "failed to save identity", err) } return nil @@ -180,19 +179,19 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error // Load peer's public key peerPubKey, err := stmf.LoadPublicKeyBase64(peerPubKeyBase64) if err != nil { - return nil, coreerr.E("NodeManager.DeriveSharedSecret", "failed to load peer public key", err) + return nil, core.E("NodeManager.DeriveSharedSecret", "failed to load peer public key", err) } // Load our private key privateKey, err := ecdh.X25519().NewPrivateKey(n.privateKey) if err != nil { - return nil, coreerr.E("NodeManager.DeriveSharedSecret", "failed to load private key", err) + return nil, core.E("NodeManager.DeriveSharedSecret", "failed to load private key", err) } // Derive shared secret using ECDH sharedSecret, err := privateKey.ECDH(peerPubKey) if err != nil { - return nil, coreerr.E("NodeManager.DeriveSharedSecret", "failed to derive shared secret", err) + return nil, core.E("NodeManager.DeriveSharedSecret", "failed to derive shared secret", err) } // Hash the shared secret using SHA-256 (same pattern as Borg/trix) @@ -204,13 +203,13 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error func (n *NodeManager) savePrivateKey() error { // Ensure directory exists dir := filepath.Dir(n.keyPath) - if err := coreio.Local.EnsureDir(dir); err != nil { - return coreerr.E("NodeManager.savePrivateKey", "failed to create key directory", err) + if err := fsEnsureDir(dir); err != nil { + return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } // Write private key - if err := coreio.Local.Write(n.keyPath, string(n.privateKey)); err != nil { - return coreerr.E("NodeManager.savePrivateKey", "failed to write private key", err) + if err := fsWrite(n.keyPath, string(n.privateKey)); err != nil { + return core.E("NodeManager.savePrivateKey", "failed to write private key", err) } return nil @@ -220,17 +219,17 @@ func (n *NodeManager) savePrivateKey() error { func (n *NodeManager) saveIdentity() error { // Ensure directory exists dir := filepath.Dir(n.configPath) - if err := coreio.Local.EnsureDir(dir); err != nil { - return coreerr.E("NodeManager.saveIdentity", "failed to create config directory", err) + if err := fsEnsureDir(dir); err != nil { + return core.E("NodeManager.saveIdentity", "failed to create config directory", err) } data, err := json.MarshalIndent(n.identity, "", " ") if err != nil { - return coreerr.E("NodeManager.saveIdentity", "failed to marshal identity", err) + return core.E("NodeManager.saveIdentity", "failed to marshal identity", err) } - if err := coreio.Local.Write(n.configPath, string(data)); err != nil { - return coreerr.E("NodeManager.saveIdentity", "failed to write identity", err) + if err := fsWrite(n.configPath, string(data)); err != nil { + return core.E("NodeManager.saveIdentity", "failed to write identity", err) } return nil @@ -239,27 +238,27 @@ func (n *NodeManager) saveIdentity() error { // loadIdentity loads the node identity from disk. func (n *NodeManager) loadIdentity() error { // Load identity config - content, err := coreio.Local.Read(n.configPath) + content, err := fsRead(n.configPath) if err != nil { - return coreerr.E("NodeManager.loadIdentity", "failed to read identity", err) + return core.E("NodeManager.loadIdentity", "failed to read identity", err) } var identity NodeIdentity if err := json.Unmarshal([]byte(content), &identity); err != nil { - return coreerr.E("NodeManager.loadIdentity", "failed to unmarshal identity", err) + return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", err) } // Load private key - keyContent, err := coreio.Local.Read(n.keyPath) + keyContent, err := fsRead(n.keyPath) if err != nil { - return coreerr.E("NodeManager.loadIdentity", "failed to read private key", err) + return core.E("NodeManager.loadIdentity", "failed to read private key", err) } privateKey := []byte(keyContent) // Reconstruct keypair from private key keyPair, err := stmf.LoadKeyPair(privateKey) if err != nil { - return coreerr.E("NodeManager.loadIdentity", "failed to load keypair", err) + return core.E("NodeManager.loadIdentity", "failed to load keypair", err) } n.identity = &identity @@ -275,16 +274,16 @@ func (n *NodeManager) Delete() error { defer n.mu.Unlock() // Remove private key (ignore if already absent) - if coreio.Local.Exists(n.keyPath) { - if err := coreio.Local.Delete(n.keyPath); err != nil { - return coreerr.E("NodeManager.Delete", "failed to remove private key", err) + if fsExists(n.keyPath) { + if err := fsDelete(n.keyPath); err != nil { + return core.E("NodeManager.Delete", "failed to remove private key", err) } } // Remove identity config (ignore if already absent) - if coreio.Local.Exists(n.configPath) { - if err := coreio.Local.Delete(n.configPath); err != nil { - return coreerr.E("NodeManager.Delete", "failed to remove identity", err) + if fsExists(n.configPath) { + if err := fsDelete(n.configPath); err != nil { + return core.E("NodeManager.Delete", "failed to remove identity", err) } } diff --git a/node/levin/header.go b/node/levin/header.go index e93531f..189782a 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -8,7 +8,7 @@ package levin import ( "encoding/binary" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // HeaderSize is the exact byte length of a serialised Levin header. @@ -43,8 +43,8 @@ const ( // Sentinel errors returned by DecodeHeader. var ( - ErrBadSignature = coreerr.E("levin", "bad signature", nil) - ErrPayloadTooBig = coreerr.E("levin", "payload exceeds maximum size", nil) + ErrBadSignature = core.E("levin", "bad signature", nil) + ErrPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) // Header is the 33-byte packed header that prefixes every Levin message. diff --git a/node/levin/storage.go b/node/levin/storage.go index 3d39718..d3d2499 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -10,7 +10,7 @@ import ( "math" "slices" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // Portable storage signatures and version (9-byte header). @@ -41,12 +41,12 @@ const ( // Sentinel errors for storage encoding and decoding. var ( - ErrStorageBadSignature = coreerr.E("levin.storage", "bad storage signature", nil) - ErrStorageTruncated = coreerr.E("levin.storage", "truncated storage data", nil) - ErrStorageBadVersion = coreerr.E("levin.storage", "unsupported storage version", nil) - ErrStorageNameTooLong = coreerr.E("levin.storage", "entry name exceeds 255 bytes", nil) - ErrStorageTypeMismatch = coreerr.E("levin.storage", "value type mismatch", nil) - ErrStorageUnknownType = coreerr.E("levin.storage", "unknown type tag", nil) + ErrStorageBadSignature = core.E("levin.storage", "bad storage signature", nil) + ErrStorageTruncated = core.E("levin.storage", "truncated storage data", nil) + ErrStorageBadVersion = core.E("levin.storage", "unsupported storage version", nil) + ErrStorageNameTooLong = core.E("levin.storage", "entry name exceeds 255 bytes", nil) + ErrStorageTypeMismatch = core.E("levin.storage", "value type mismatch", nil) + ErrStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) ) // Section is an ordered map of named values forming a portable storage section. @@ -394,7 +394,7 @@ func encodeValue(buf []byte, v Value) ([]byte, error) { return encodeSection(buf, v.objectVal) default: - return nil, coreerr.E("levin.encodeValue", fmt.Sprintf("unknown type tag: 0x%02x", v.Type), ErrStorageUnknownType) + return nil, core.E("levin.encodeValue", fmt.Sprintf("unknown type tag: 0x%02x", v.Type), ErrStorageUnknownType) } } @@ -441,7 +441,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { return buf, nil default: - return nil, coreerr.E("levin.encodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return nil, core.E("levin.encodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) } } @@ -476,7 +476,7 @@ func DecodeStorage(data []byte) (Section, error) { func decodeSection(buf []byte) (Section, int, error) { count, n, err := UnpackVarint(buf) if err != nil { - return nil, 0, coreerr.E("levin.decodeSection", "section entry count", err) + return nil, 0, core.E("levin.decodeSection", "section entry count", err) } off := n @@ -507,7 +507,7 @@ func decodeSection(buf []byte) (Section, int, error) { // Value. val, consumed, err := decodeValue(buf[off:], tag) if err != nil { - return nil, 0, coreerr.E("levin.decodeSection", "field "+name, err) + return nil, 0, core.E("levin.decodeSection", "field "+name, err) } off += consumed @@ -613,7 +613,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { return Value{Type: TypeObject, objectVal: sec}, consumed, nil default: - return Value{}, 0, coreerr.E("levin.decodeValue", fmt.Sprintf("unknown type tag: 0x%02x", tag), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeValue", fmt.Sprintf("unknown type tag: 0x%02x", tag), ErrStorageUnknownType) } } @@ -681,6 +681,6 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { return Value{Type: tag, objectArray: arr}, off, nil default: - return Value{}, 0, coreerr.E("levin.decodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) } } diff --git a/node/levin/varint.go b/node/levin/varint.go index 2830e71..edbe7e7 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -6,27 +6,27 @@ package levin import ( "encoding/binary" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // Size-mark bits occupying the two lowest bits of the first byte. const ( - varintMask = 0x03 - varintMark1 = 0x00 // 1 byte, max 63 - varintMark2 = 0x01 // 2 bytes, max 16,383 - varintMark4 = 0x02 // 4 bytes, max 1,073,741,823 - varintMark8 = 0x03 // 8 bytes, max 4,611,686,018,427,387,903 - varintMax1 = 63 - varintMax2 = 16_383 - varintMax4 = 1_073_741_823 - varintMax8 = 4_611_686_018_427_387_903 + varintMask = 0x03 + varintMark1 = 0x00 // 1 byte, max 63 + varintMark2 = 0x01 // 2 bytes, max 16,383 + varintMark4 = 0x02 // 4 bytes, max 1,073,741,823 + varintMark8 = 0x03 // 8 bytes, max 4,611,686,018,427,387,903 + varintMax1 = 63 + varintMax2 = 16_383 + varintMax4 = 1_073_741_823 + varintMax8 = 4_611_686_018_427_387_903 ) // ErrVarintTruncated is returned when the buffer is too short. -var ErrVarintTruncated = coreerr.E("levin", "truncated varint", nil) +var ErrVarintTruncated = core.E("levin", "truncated varint", nil) // ErrVarintOverflow is returned when the value is too large to encode. -var ErrVarintOverflow = coreerr.E("levin", "varint overflow", nil) +var ErrVarintOverflow = core.E("levin", "varint overflow", nil) // PackVarint encodes v using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; diff --git a/node/peer.go b/node/peer.go index d4ff02c..e876cd6 100644 --- a/node/peer.go +++ b/node/peer.go @@ -10,8 +10,7 @@ import ( "sync" "time" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/p2p/logging" poindexter "forge.lthn.ai/Snider/Poindexter" @@ -79,13 +78,13 @@ func validatePeerName(name string) error { return nil // Empty names are allowed (optional field) } if len(name) < PeerNameMinLength { - return coreerr.E("validatePeerName", "peer name too short", nil) + return core.E("validatePeerName", "peer name too short", nil) } if len(name) > PeerNameMaxLength { - return coreerr.E("validatePeerName", "peer name too long", nil) + return core.E("validatePeerName", "peer name too long", nil) } if !peerNameRegex.MatchString(name) { - return coreerr.E("validatePeerName", "peer name contains invalid characters (use alphanumeric, hyphens, underscores, spaces)", nil) + return core.E("validatePeerName", "peer name contains invalid characters (use alphanumeric, hyphens, underscores, spaces)", nil) } return nil } @@ -123,7 +122,7 @@ var ( func NewPeerRegistry() (*PeerRegistry, error) { peersPath, err := xdg.ConfigFile("lethean-desktop/peers.json") if err != nil { - return nil, coreerr.E("PeerRegistry.New", "failed to get peers path", err) + return nil, core.E("PeerRegistry.New", "failed to get peers path", err) } return NewPeerRegistryWithPath(peersPath) @@ -244,7 +243,7 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { if peer.ID == "" { r.mu.Unlock() - return coreerr.E("PeerRegistry.AddPeer", "peer ID is required", nil) + return core.E("PeerRegistry.AddPeer", "peer ID is required", nil) } // Validate peer name (P2P-LOW-3) @@ -255,7 +254,7 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { if _, exists := r.peers[peer.ID]; exists { r.mu.Unlock() - return coreerr.E("PeerRegistry.AddPeer", "peer "+peer.ID+" already exists", nil) + return core.E("PeerRegistry.AddPeer", "peer "+peer.ID+" already exists", nil) } // Set defaults @@ -280,7 +279,7 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { if _, exists := r.peers[peer.ID]; !exists { r.mu.Unlock() - return coreerr.E("PeerRegistry.UpdatePeer", "peer "+peer.ID+" not found", nil) + return core.E("PeerRegistry.UpdatePeer", "peer "+peer.ID+" not found", nil) } r.peers[peer.ID] = peer @@ -297,7 +296,7 @@ func (r *PeerRegistry) RemovePeer(id string) error { if _, exists := r.peers[id]; !exists { r.mu.Unlock() - return coreerr.E("PeerRegistry.RemovePeer", "peer "+id+" not found", nil) + return core.E("PeerRegistry.RemovePeer", "peer "+id+" not found", nil) } delete(r.peers, id) @@ -351,7 +350,7 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMS, geoKM float64, hops int) peer, exists := r.peers[id] if !exists { r.mu.Unlock() - return coreerr.E("PeerRegistry.UpdateMetrics", "peer "+id+" not found", nil) + return core.E("PeerRegistry.UpdateMetrics", "peer "+id+" not found", nil) } peer.PingMS = pingMS @@ -373,7 +372,7 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { peer, exists := r.peers[id] if !exists { r.mu.Unlock() - return coreerr.E("PeerRegistry.UpdateScore", "peer "+id+" not found", nil) + return core.E("PeerRegistry.UpdateScore", "peer "+id+" not found", nil) } // Clamp score to 0-100 @@ -656,8 +655,8 @@ func (r *PeerRegistry) scheduleSave() { func (r *PeerRegistry) saveNow() error { // Ensure directory exists dir := filepath.Dir(r.path) - if err := coreio.Local.EnsureDir(dir); err != nil { - return coreerr.E("PeerRegistry.saveNow", "failed to create peers directory", err) + if err := fsEnsureDir(dir); err != nil { + return core.E("PeerRegistry.saveNow", "failed to create peers directory", err) } // Convert to slice for JSON @@ -665,18 +664,18 @@ func (r *PeerRegistry) saveNow() error { data, err := json.MarshalIndent(peers, "", " ") if err != nil { - return coreerr.E("PeerRegistry.saveNow", "failed to marshal peers", err) + return core.E("PeerRegistry.saveNow", "failed to marshal peers", err) } // Use atomic write pattern: write to temp file, then rename tmpPath := r.path + ".tmp" - if err := coreio.Local.Write(tmpPath, string(data)); err != nil { - return coreerr.E("PeerRegistry.saveNow", "failed to write peers temp file", err) + if err := fsWrite(tmpPath, string(data)); err != nil { + return core.E("PeerRegistry.saveNow", "failed to write peers temp file", err) } - if err := coreio.Local.Rename(tmpPath, r.path); err != nil { - coreio.Local.Delete(tmpPath) // Clean up temp file - return coreerr.E("PeerRegistry.saveNow", "failed to rename peers file", err) + if err := fsRename(tmpPath, r.path); err != nil { + fsDelete(tmpPath) // Clean up temp file + return core.E("PeerRegistry.saveNow", "failed to rename peers file", err) } return nil @@ -718,14 +717,14 @@ func (r *PeerRegistry) save() error { // load reads peers from disk. func (r *PeerRegistry) load() error { - content, err := coreio.Local.Read(r.path) + content, err := fsRead(r.path) if err != nil { - return coreerr.E("PeerRegistry.load", "failed to read peers", err) + return core.E("PeerRegistry.load", "failed to read peers", err) } var peers []*Peer if err := json.Unmarshal([]byte(content), &peers); err != nil { - return coreerr.E("PeerRegistry.load", "failed to unmarshal peers", err) + return core.E("PeerRegistry.load", "failed to unmarshal peers", err) } r.peers = make(map[string]*Peer) diff --git a/node/protocol.go b/node/protocol.go index 80ca346..f93b2f2 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -3,7 +3,7 @@ package node import ( "fmt" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // ProtocolError represents an error from the remote peer. @@ -26,7 +26,7 @@ type ResponseHandler struct{} // 3. If response type matches expected (returns error if not) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { - return coreerr.E("ResponseHandler.ValidateResponse", "nil response", nil) + return core.E("ResponseHandler.ValidateResponse", "nil response", nil) } // Check for error response @@ -40,7 +40,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy // Check expected type if resp.Type != expectedType { - return coreerr.E("ResponseHandler.ValidateResponse", "unexpected response type: expected "+string(expectedType)+", got "+string(resp.Type), nil) + return core.E("ResponseHandler.ValidateResponse", "unexpected response type: expected "+string(expectedType)+", got "+string(resp.Type), nil) } return nil @@ -55,7 +55,7 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, if target != nil { if err := resp.ParsePayload(target); err != nil { - return coreerr.E("ResponseHandler.ParseResponse", "failed to parse "+string(expectedType)+" payload", err) + return core.E("ResponseHandler.ParseResponse", "failed to parse "+string(expectedType)+" payload", err) } } diff --git a/node/transport.go b/node/transport.go index e30c0e5..ceb1cb1 100644 --- a/node/transport.go +++ b/node/transport.go @@ -15,7 +15,7 @@ import ( "sync/atomic" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/p2p/logging" "forge.lthn.ai/Snider/Borg/pkg/smsg" @@ -290,7 +290,7 @@ func (t *Transport) Stop() error { defer cancel() if err := t.server.Shutdown(ctx); err != nil { - return coreerr.E("Transport.Stop", "server shutdown error", err) + return core.E("Transport.Stop", "server shutdown error", err) } } @@ -321,7 +321,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } conn, _, err := dialer.Dial(u.String(), nil) if err != nil { - return nil, coreerr.E("Transport.Connect", "failed to connect to peer", err) + return nil, core.E("Transport.Connect", "failed to connect to peer", err) } pc := &PeerConnection{ @@ -336,7 +336,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // This also derives and stores the shared secret in pc.SharedSecret if err := t.performHandshake(pc); err != nil { conn.Close() - return nil, coreerr.E("Transport.Connect", "handshake failed", err) + return nil, core.E("Transport.Connect", "handshake failed", err) } // Store connection using the real peer ID from handshake @@ -369,7 +369,7 @@ func (t *Transport) Send(peerID string, msg *Message) error { t.mu.RUnlock() if !exists { - return coreerr.E("Transport.Send", "peer "+peerID+" not connected", nil) + return core.E("Transport.Send", "peer "+peerID+" not connected", nil) } return pc.Send(msg) @@ -629,7 +629,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { // Generate challenge for the server to prove it has the matching private key challenge, err := GenerateChallenge() if err != nil { - return coreerr.E("Transport.performHandshake", "generate challenge", err) + return core.E("Transport.performHandshake", "generate challenge", err) } payload := HandshakePayload{ @@ -640,41 +640,41 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { msg, err := NewMessage(MsgHandshake, identity.ID, pc.Peer.ID, payload) if err != nil { - return coreerr.E("Transport.performHandshake", "create handshake message", err) + return core.E("Transport.performHandshake", "create handshake message", err) } // First message is unencrypted (peer needs our public key) data, err := MarshalJSON(msg) if err != nil { - return coreerr.E("Transport.performHandshake", "marshal handshake message", err) + return core.E("Transport.performHandshake", "marshal handshake message", err) } if err := pc.Conn.WriteMessage(websocket.TextMessage, data); err != nil { - return coreerr.E("Transport.performHandshake", "send handshake", err) + return core.E("Transport.performHandshake", "send handshake", err) } // Wait for ack _, ackData, err := pc.Conn.ReadMessage() if err != nil { - return coreerr.E("Transport.performHandshake", "read handshake ack", err) + return core.E("Transport.performHandshake", "read handshake ack", err) } var ackMsg Message if err := json.Unmarshal(ackData, &ackMsg); err != nil { - return coreerr.E("Transport.performHandshake", "unmarshal handshake ack", err) + return core.E("Transport.performHandshake", "unmarshal handshake ack", err) } if ackMsg.Type != MsgHandshakeAck { - return coreerr.E("Transport.performHandshake", "expected handshake_ack, got "+string(ackMsg.Type), nil) + return core.E("Transport.performHandshake", "expected handshake_ack, got "+string(ackMsg.Type), nil) } var ackPayload HandshakeAckPayload if err := ackMsg.ParsePayload(&ackPayload); err != nil { - return coreerr.E("Transport.performHandshake", "parse handshake ack payload", err) + return core.E("Transport.performHandshake", "parse handshake ack payload", err) } if !ackPayload.Accepted { - return coreerr.E("Transport.performHandshake", "handshake rejected: "+ackPayload.Reason, nil) + return core.E("Transport.performHandshake", "handshake rejected: "+ackPayload.Reason, nil) } // Update peer with the received identity info @@ -686,15 +686,15 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { // Verify challenge response - derive shared secret first using the peer's public key sharedSecret, err := t.node.DeriveSharedSecret(pc.Peer.PublicKey) if err != nil { - return coreerr.E("Transport.performHandshake", "derive shared secret for challenge verification", err) + return core.E("Transport.performHandshake", "derive shared secret for challenge verification", err) } // Verify the server's response to our challenge if len(ackPayload.ChallengeResponse) == 0 { - return coreerr.E("Transport.performHandshake", "server did not provide challenge response", nil) + return core.E("Transport.performHandshake", "server did not provide challenge response", nil) } if !VerifyChallenge(challenge, ackPayload.ChallengeResponse, sharedSecret) { - return coreerr.E("Transport.performHandshake", "challenge response verification failed: server may not have matching private key", nil) + return core.E("Transport.performHandshake", "challenge response verification failed: server may not have matching private key", nil) } // Store the shared secret for later use @@ -841,7 +841,7 @@ func (pc *PeerConnection) Send(msg *Message) error { // Set write deadline to prevent blocking forever if err := pc.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { - return coreerr.E("PeerConnection.Send", "failed to set write deadline", err) + return core.E("PeerConnection.Send", "failed to set write deadline", err) } defer pc.Conn.SetWriteDeadline(time.Time{}) // Reset deadline after send diff --git a/node/worker.go b/node/worker.go index af917d4..1ea18ea 100644 --- a/node/worker.go +++ b/node/worker.go @@ -6,7 +6,7 @@ import ( "path/filepath" "time" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "dappco.re/go/core/p2p/logging" "github.com/adrg/xdg" @@ -55,7 +55,6 @@ func NewWorker(node *NodeManager, transport *Transport) *Worker { } } - // SetMinerManager sets the miner manager for handling miner operations. func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager @@ -119,7 +118,7 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { func (w *Worker) handlePing(msg *Message) (*Message, error) { var ping PingPayload if err := msg.ParsePayload(&ping); err != nil { - return nil, coreerr.E("Worker.handlePing", "invalid ping payload", err) + return nil, core.E("Worker.handlePing", "invalid ping payload", err) } pong := PongPayload{ @@ -202,12 +201,12 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { var payload StartMinerPayload if err := msg.ParsePayload(&payload); err != nil { - return nil, coreerr.E("Worker.handleStartMiner", "invalid start miner payload", err) + return nil, core.E("Worker.handleStartMiner", "invalid start miner payload", err) } // Validate miner type is provided if payload.MinerType == "" { - return nil, coreerr.E("Worker.handleStartMiner", "miner type is required", nil) + return nil, core.E("Worker.handleStartMiner", "miner type is required", nil) } // Get the config from the profile or use the override @@ -217,11 +216,11 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { } else if w.profileManager != nil { profile, err := w.profileManager.GetProfile(payload.ProfileID) if err != nil { - return nil, coreerr.E("Worker.handleStartMiner", "profile not found: "+payload.ProfileID, nil) + return nil, core.E("Worker.handleStartMiner", "profile not found: "+payload.ProfileID, nil) } config = profile } else { - return nil, coreerr.E("Worker.handleStartMiner", "no config provided and no profile manager configured", nil) + return nil, core.E("Worker.handleStartMiner", "no config provided and no profile manager configured", nil) } // Start the miner @@ -249,7 +248,7 @@ func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { var payload StopMinerPayload if err := msg.ParsePayload(&payload); err != nil { - return nil, coreerr.E("Worker.handleStopMiner", "invalid stop miner payload", err) + return nil, core.E("Worker.handleStopMiner", "invalid stop miner payload", err) } err := w.minerManager.StopMiner(payload.MinerName) @@ -272,7 +271,7 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { var payload GetLogsPayload if err := msg.ParsePayload(&payload); err != nil { - return nil, coreerr.E("Worker.handleGetLogs", "invalid get logs payload", err) + return nil, core.E("Worker.handleGetLogs", "invalid get logs payload", err) } // Validate and limit the Lines parameter to prevent resource exhaustion @@ -283,7 +282,7 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { miner, err := w.minerManager.GetMiner(payload.MinerName) if err != nil { - return nil, coreerr.E("Worker.handleGetLogs", "miner not found: "+payload.MinerName, nil) + return nil, core.E("Worker.handleGetLogs", "miner not found: "+payload.MinerName, nil) } lines := miner.GetConsoleHistory(payload.Lines) @@ -301,7 +300,7 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, error) { var payload DeployPayload if err := msg.ParsePayload(&payload); err != nil { - return nil, coreerr.E("Worker.handleDeploy", "invalid deploy payload", err) + return nil, core.E("Worker.handleDeploy", "invalid deploy payload", err) } // Reconstruct Bundle object from payload @@ -321,19 +320,19 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err switch bundle.Type { case BundleProfile: if w.profileManager == nil { - return nil, coreerr.E("Worker.handleDeploy", "profile manager not configured", nil) + return nil, core.E("Worker.handleDeploy", "profile manager not configured", nil) } // Decrypt and extract profile data profileData, err := ExtractProfileBundle(bundle, password) if err != nil { - return nil, coreerr.E("Worker.handleDeploy", "failed to extract profile bundle", err) + return nil, core.E("Worker.handleDeploy", "failed to extract profile bundle", err) } // Unmarshal into interface{} to pass to ProfileManager var profile any if err := json.Unmarshal(profileData, &profile); err != nil { - return nil, coreerr.E("Worker.handleDeploy", "invalid profile data JSON", err) + return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", err) } if err := w.profileManager.SaveProfile(profile); err != nil { @@ -366,7 +365,7 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err // Extract miner bundle minerPath, profileData, err := ExtractMinerBundle(bundle, password, installDir) if err != nil { - return nil, coreerr.E("Worker.handleDeploy", "failed to extract miner bundle", err) + return nil, core.E("Worker.handleDeploy", "failed to extract miner bundle", err) } // If the bundle contained a profile config, save it @@ -396,7 +395,7 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err return msg.Reply(MsgDeployAck, ack) default: - return nil, coreerr.E("Worker.handleDeploy", "unknown bundle type: "+payload.BundleType, nil) + return nil, core.E("Worker.handleDeploy", "unknown bundle type: "+payload.BundleType, nil) } } diff --git a/ueps/packet.go b/ueps/packet.go index 0fb590c..7241b2a 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -7,7 +7,7 @@ import ( "encoding/binary" "io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // TLV Types @@ -23,7 +23,7 @@ const ( // UEPSHeader represents the conscious routing metadata type UEPSHeader struct { - Version uint8 // Default 0x09 + Version uint8 // Default 0x09 CurrentLayer uint8 TargetLayer uint8 IntentID uint8 // Semantic Token @@ -68,7 +68,7 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { if err := writeTLV(buf, TagIntent, []byte{p.Header.IntentID}); err != nil { return nil, err } - + // Threat Score is uint16, needs binary packing tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) @@ -105,22 +105,21 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { func writeTLV(w io.Writer, tag uint8, value []byte) error { // Check length constraint (2 byte length = max 65535 bytes) if len(value) > 65535 { - return coreerr.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) + return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } if _, err := w.Write([]byte{tag}); err != nil { return err } - + lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(len(value))) if _, err := w.Write(lenBuf); err != nil { return err } - + if _, err := w.Write(value); err != nil { return err } return nil } - diff --git a/ueps/reader.go b/ueps/reader.go index dcd1fe7..8546804 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -8,7 +8,7 @@ import ( "encoding/binary" "io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // ParsedPacket holds the verified data @@ -93,7 +93,7 @@ func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) verify: if len(signature) == 0 { - return nil, coreerr.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil) + return nil, core.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil) } // 5. Verify HMAC @@ -104,7 +104,7 @@ verify: expectedMAC := mac.Sum(nil) if !hmac.Equal(signature, expectedMAC) { - return nil, coreerr.E("ueps.ReadAndVerify", "integrity violation: HMAC mismatch (ThreatScore +100)", nil) + return nil, core.E("ueps.ReadAndVerify", "integrity violation: HMAC mismatch (ThreatScore +100)", nil) } return &ParsedPacket{ @@ -112,4 +112,3 @@ verify: Payload: payload, }, nil } - -- 2.45.3 From 04ae11da43672a2a6d60e4eff9684a26b0697050 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 14:37:02 +0000 Subject: [PATCH 02/60] refactor(node): replace stdlib helpers with core primitives Co-Authored-By: Virgil --- logging/logger.go | 18 ++++++-------- node/bufpool.go | 34 +++++++++++-------------- node/bundle.go | 58 +++++++++++++++++++++++++++---------------- node/controller.go | 3 +-- node/dispatcher.go | 9 +++---- node/identity.go | 18 +++++++------- node/levin/storage.go | 9 +++---- node/message.go | 32 +++++++++++++++--------- node/peer.go | 16 ++++++------ node/protocol.go | 4 +-- node/transport.go | 14 +++++------ node/worker.go | 14 +++++------ 12 files changed, 118 insertions(+), 111 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index d368b82..ddb0459 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -2,11 +2,9 @@ package logging import ( - "fmt" "io" "maps" "os" - "strings" "sync" "time" @@ -115,7 +113,7 @@ func (l *Logger) log(level Level, msg string, fields Fields) { } // Build the log line - var sb strings.Builder + sb := core.NewBuilder() timestamp := time.Now().Format("2006/01/02 15:04:05") sb.WriteString(timestamp) sb.WriteString(" [") @@ -138,12 +136,12 @@ func (l *Logger) log(level Level, msg string, fields Fields) { sb.WriteString(" ") sb.WriteString(k) sb.WriteString("=") - sb.WriteString(fmt.Sprintf("%v", v)) + sb.WriteString(core.Sprint(v)) } } sb.WriteString("\n") - fmt.Fprint(l.output, sb.String()) + _, _ = l.output.Write([]byte(sb.String())) } // Debug logs a debug message. @@ -168,22 +166,22 @@ func (l *Logger) Error(msg string, fields ...Fields) { // Debugf logs a formatted debug message. func (l *Logger) Debugf(format string, args ...any) { - l.log(LevelDebug, fmt.Sprintf(format, args...), nil) + l.log(LevelDebug, core.Sprintf(format, args...), nil) } // Infof logs a formatted informational message. func (l *Logger) Infof(format string, args ...any) { - l.log(LevelInfo, fmt.Sprintf(format, args...), nil) + l.log(LevelInfo, core.Sprintf(format, args...), nil) } // Warnf logs a formatted warning message. func (l *Logger) Warnf(format string, args ...any) { - l.log(LevelWarn, fmt.Sprintf(format, args...), nil) + l.log(LevelWarn, core.Sprintf(format, args...), nil) } // Errorf logs a formatted error message. func (l *Logger) Errorf(format string, args ...any) { - l.log(LevelError, fmt.Sprintf(format, args...), nil) + l.log(LevelError, core.Sprintf(format, args...), nil) } // mergeFields combines multiple Fields maps into one. @@ -270,7 +268,7 @@ func Errorf(format string, args ...any) { // ParseLevel parses a string into a log level. func ParseLevel(s string) (Level, error) { - switch strings.ToUpper(s) { + switch core.Upper(s) { case "DEBUG": return LevelDebug, nil case "INFO": diff --git a/node/bufpool.go b/node/bufpool.go index 7848214..e7244c2 100644 --- a/node/bufpool.go +++ b/node/bufpool.go @@ -2,8 +2,9 @@ package node import ( "bytes" - "encoding/json" "sync" + + core "dappco.re/go/core" ) // bufferPool provides reusable byte buffers for JSON encoding. @@ -29,27 +30,22 @@ func putBuffer(buf *bytes.Buffer) { } } -// MarshalJSON encodes a value to JSON using a pooled buffer. +// MarshalJSON encodes a value to JSON using Core's JSON primitive and then +// restores the historical no-EscapeHTML behaviour expected by the node package. // Returns a copy of the encoded bytes (safe to use after the function returns). func MarshalJSON(v any) ([]byte, error) { - buf := getBuffer() - defer putBuffer(buf) - - enc := json.NewEncoder(buf) - // Don't escape HTML characters (matches json.Marshal behavior for these use cases) - enc.SetEscapeHTML(false) - if err := enc.Encode(v); err != nil { - return nil, err + encoded := core.JSONMarshal(v) + if !encoded.OK { + return nil, encoded.Value.(error) } + data := encoded.Value.([]byte) - // json.Encoder.Encode adds a newline; remove it to match json.Marshal - data := buf.Bytes() - if len(data) > 0 && data[len(data)-1] == '\n' { - data = data[:len(data)-1] - } + data = bytes.ReplaceAll(data, []byte(`\u003c`), []byte("<")) + data = bytes.ReplaceAll(data, []byte(`\u003e`), []byte(">")) + data = bytes.ReplaceAll(data, []byte(`\u0026`), []byte("&")) - // Return a copy since the buffer will be reused - result := make([]byte, len(data)) - copy(result, data) - return result, nil + // Return a copy since callers may retain the slice after subsequent calls. + out := make([]byte, len(data)) + copy(out, data) + return out, nil } diff --git a/node/bundle.go b/node/bundle.go index e60a659..f3866c8 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -5,11 +5,8 @@ import ( "bytes" "crypto/sha256" "encoding/hex" - "encoding/json" "io" "os" - "path/filepath" - "strings" core "dappco.re/go/core" @@ -93,7 +90,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo // Create a tarball with the miner binary tarData, err := createTarball(map[string][]byte{ - filepath.Base(minerPath): minerData, + core.PathBase(minerPath): minerData, }) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create tarball", err) @@ -211,7 +208,7 @@ func createTarball(files map[string][]byte) ([]byte, error) { for name, content := range files { // Create parent directories if needed - dir := filepath.Dir(name) + dir := core.PathDir(name) if dir != "." && !dirs[dir] { hdr := &tar.Header{ Name: dir + "/", @@ -226,7 +223,7 @@ func createTarball(files map[string][]byte) ([]byte, error) { // Determine file mode (executable for binaries in miners/) mode := int64(0644) - if filepath.Dir(name) == "miners" || !isJSON(content) { + if core.PathDir(name) == "miners" || !isJSON(content) { mode = 0755 } @@ -253,11 +250,16 @@ func createTarball(files map[string][]byte) ([]byte, error) { // extractTarball extracts a tar archive to a directory, returns first executable found. func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks - absDestDir, err := filepath.Abs(destDir) - if err != nil { - return "", core.E("extractTarball", "failed to resolve destination directory", err) + absDestDir := destDir + if !core.PathIsAbs(absDestDir) { + cwd, err := os.Getwd() + if err != nil { + return "", core.E("extractTarball", "failed to resolve destination directory", err) + } + absDestDir = core.CleanPath(core.Concat(cwd, string(os.PathSeparator), absDestDir), string(os.PathSeparator)) + } else { + absDestDir = core.CleanPath(absDestDir, string(os.PathSeparator)) } - absDestDir = filepath.Clean(absDestDir) if err := fsEnsureDir(absDestDir); err != nil { return "", err @@ -276,24 +278,27 @@ func extractTarball(tarData []byte, destDir string) (string, error) { } // Security: Sanitize the tar entry name to prevent path traversal (Zip Slip) - cleanName := filepath.Clean(hdr.Name) + cleanName := core.CleanPath(hdr.Name, "/") // Reject absolute paths - if filepath.IsAbs(cleanName) { + if core.PathIsAbs(cleanName) { return "", core.E("extractTarball", "invalid tar entry: absolute path not allowed: "+hdr.Name, nil) } // Reject paths that escape the destination directory - if strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) || cleanName == ".." { + if core.HasPrefix(cleanName, "../") || cleanName == ".." { return "", core.E("extractTarball", "invalid tar entry: path traversal attempt: "+hdr.Name, nil) } // Build the full path and verify it's within destDir - fullPath := filepath.Join(absDestDir, cleanName) - fullPath = filepath.Clean(fullPath) + fullPath := core.CleanPath(core.Concat(absDestDir, string(os.PathSeparator), cleanName), string(os.PathSeparator)) // Final security check: ensure the path is still within destDir - if !strings.HasPrefix(fullPath, absDestDir+string(os.PathSeparator)) && fullPath != absDestDir { + allowedPrefix := core.Concat(absDestDir, string(os.PathSeparator)) + if absDestDir == string(os.PathSeparator) { + allowedPrefix = absDestDir + } + if !core.HasPrefix(fullPath, allowedPrefix) && fullPath != absDestDir { return "", core.E("extractTarball", "invalid tar entry: path escape attempt: "+hdr.Name, nil) } @@ -304,7 +309,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { } case tar.TypeReg: // Ensure parent directory exists - if err := fsEnsureDir(filepath.Dir(fullPath)); err != nil { + if err := fsEnsureDir(core.PathDir(fullPath)); err != nil { return "", err } @@ -345,16 +350,25 @@ func extractTarball(tarData []byte, destDir string) (string, error) { // StreamBundle writes a bundle to a writer (for large transfers). func StreamBundle(bundle *Bundle, w io.Writer) error { - encoder := json.NewEncoder(w) - return encoder.Encode(bundle) + result := core.JSONMarshal(bundle) + if !result.OK { + return result.Value.(error) + } + _, err := w.Write(result.Value.([]byte)) + return err } // ReadBundle reads a bundle from a reader. func ReadBundle(r io.Reader) (*Bundle, error) { - var bundle Bundle - decoder := json.NewDecoder(r) - if err := decoder.Decode(&bundle); err != nil { + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { return nil, err } + + var bundle Bundle + result := core.JSONUnmarshal(buf.Bytes(), &bundle) + if !result.OK { + return nil, result.Value.(error) + } return &bundle, nil } diff --git a/node/controller.go b/node/controller.go index a79bbfc..cf7f21b 100644 --- a/node/controller.go +++ b/node/controller.go @@ -2,7 +2,6 @@ package node import ( "context" - "encoding/json" "sync" "time" @@ -137,7 +136,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { } // StartRemoteMiner requests a remote peer to start a miner with a given profile. -func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride json.RawMessage) error { +func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.node.GetIdentity() if identity == nil { return ErrIdentityNotInitialized diff --git a/node/dispatcher.go b/node/dispatcher.go index a91a0ac..32899c9 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -1,7 +1,6 @@ package node import ( - "fmt" "iter" "sync" @@ -69,7 +68,7 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { defer d.mu.Unlock() d.handlers[intentID] = handler d.log.Debug("handler registered", logging.Fields{ - "intent_id": fmt.Sprintf("0x%02X", intentID), + "intent_id": core.Sprintf("0x%02X", intentID), }) } @@ -108,7 +107,7 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { d.log.Warn("packet dropped: threat score exceeds safety threshold", logging.Fields{ "threat_score": pkt.Header.ThreatScore, "threshold": ThreatScoreThreshold, - "intent_id": fmt.Sprintf("0x%02X", pkt.Header.IntentID), + "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), "version": pkt.Header.Version, }) return ErrThreatScoreExceeded @@ -121,7 +120,7 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { if !exists { d.log.Warn("packet dropped: unknown intent", logging.Fields{ - "intent_id": fmt.Sprintf("0x%02X", pkt.Header.IntentID), + "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), "version": pkt.Header.Version, }) return ErrUnknownIntent @@ -134,7 +133,7 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { var ( // ErrThreatScoreExceeded is returned when a packet's ThreatScore exceeds // the safety threshold. - ErrThreatScoreExceeded = core.E("Dispatcher.Dispatch", fmt.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) + ErrThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) // ErrUnknownIntent is returned when no handler is registered for the // packet's IntentID. diff --git a/node/identity.go b/node/identity.go index d745c5c..4e4f255 100644 --- a/node/identity.go +++ b/node/identity.go @@ -7,8 +7,6 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" - "encoding/json" - "path/filepath" "sync" "time" @@ -202,7 +200,7 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error // savePrivateKey saves the private key to disk with restricted permissions. func (n *NodeManager) savePrivateKey() error { // Ensure directory exists - dir := filepath.Dir(n.keyPath) + dir := core.PathDir(n.keyPath) if err := fsEnsureDir(dir); err != nil { return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } @@ -218,15 +216,16 @@ func (n *NodeManager) savePrivateKey() error { // saveIdentity saves the public identity to the config file. func (n *NodeManager) saveIdentity() error { // Ensure directory exists - dir := filepath.Dir(n.configPath) + dir := core.PathDir(n.configPath) if err := fsEnsureDir(dir); err != nil { return core.E("NodeManager.saveIdentity", "failed to create config directory", err) } - data, err := json.MarshalIndent(n.identity, "", " ") - if err != nil { - return core.E("NodeManager.saveIdentity", "failed to marshal identity", err) + result := core.JSONMarshal(n.identity) + if !result.OK { + return core.E("NodeManager.saveIdentity", "failed to marshal identity", result.Value.(error)) } + data := result.Value.([]byte) if err := fsWrite(n.configPath, string(data)); err != nil { return core.E("NodeManager.saveIdentity", "failed to write identity", err) @@ -244,8 +243,9 @@ func (n *NodeManager) loadIdentity() error { } var identity NodeIdentity - if err := json.Unmarshal([]byte(content), &identity); err != nil { - return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", err) + result := core.JSONUnmarshalString(content, &identity) + if !result.OK { + return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", result.Value.(error)) } // Load private key diff --git a/node/levin/storage.go b/node/levin/storage.go index d3d2499..f8896f4 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -5,7 +5,6 @@ package levin import ( "encoding/binary" - "fmt" "maps" "math" "slices" @@ -394,7 +393,7 @@ func encodeValue(buf []byte, v Value) ([]byte, error) { return encodeSection(buf, v.objectVal) default: - return nil, core.E("levin.encodeValue", fmt.Sprintf("unknown type tag: 0x%02x", v.Type), ErrStorageUnknownType) + return nil, core.E("levin.encodeValue", core.Sprintf("unknown type tag: 0x%02x", v.Type), ErrStorageUnknownType) } } @@ -441,7 +440,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { return buf, nil default: - return nil, core.E("levin.encodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return nil, core.E("levin.encodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) } } @@ -613,7 +612,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { return Value{Type: TypeObject, objectVal: sec}, consumed, nil default: - return Value{}, 0, core.E("levin.decodeValue", fmt.Sprintf("unknown type tag: 0x%02x", tag), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeValue", core.Sprintf("unknown type tag: 0x%02x", tag), ErrStorageUnknownType) } } @@ -681,6 +680,6 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { return Value{Type: tag, objectArray: arr}, off, nil default: - return Value{}, 0, core.E("levin.decodeArray", fmt.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) } } diff --git a/node/message.go b/node/message.go index d4b2b6e..b90ef0b 100644 --- a/node/message.go +++ b/node/message.go @@ -5,6 +5,7 @@ import ( "slices" "time" + core "dappco.re/go/core" "github.com/google/uuid" ) @@ -20,6 +21,9 @@ const ( // Used for version negotiation during handshake. var SupportedProtocolVersions = []string{"1.0"} +// RawMessage is the message payload byte slice used for deferred JSON decoding. +type RawMessage = json.RawMessage + // IsProtocolVersionSupported checks if a given version is supported. func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) @@ -57,18 +61,18 @@ const ( // Message represents a P2P message between nodes. type Message struct { - ID string `json:"id"` // UUID - Type MessageType `json:"type"` - From string `json:"from"` // Sender node ID - To string `json:"to"` // Recipient node ID (empty for broadcast) - Timestamp time.Time `json:"ts"` - Payload json.RawMessage `json:"payload"` - ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to + ID string `json:"id"` // UUID + Type MessageType `json:"type"` + From string `json:"from"` // Sender node ID + To string `json:"to"` // Recipient node ID (empty for broadcast) + Timestamp time.Time `json:"ts"` + Payload RawMessage `json:"payload"` + ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to } // NewMessage creates a new message with a generated ID and timestamp. func NewMessage(msgType MessageType, from, to string, payload any) (*Message, error) { - var payloadBytes json.RawMessage + var payloadBytes RawMessage if payload != nil { data, err := MarshalJSON(payload) if err != nil { @@ -102,7 +106,11 @@ func (m *Message) ParsePayload(v any) error { if m.Payload == nil { return nil } - return json.Unmarshal(m.Payload, v) + result := core.JSONUnmarshal(m.Payload, v) + if !result.OK { + return result.Value.(error) + } + return nil } // --- Payload Types --- @@ -135,9 +143,9 @@ type PongPayload struct { // StartMinerPayload requests starting a miner. type StartMinerPayload struct { - MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") - ProfileID string `json:"profileId,omitempty"` - Config json.RawMessage `json:"config,omitempty"` // Override profile config + MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") + ProfileID string `json:"profileId,omitempty"` + Config RawMessage `json:"config,omitempty"` // Override profile config } // StopMinerPayload requests stopping a miner. diff --git a/node/peer.go b/node/peer.go index e876cd6..cedc741 100644 --- a/node/peer.go +++ b/node/peer.go @@ -1,10 +1,8 @@ package node import ( - "encoding/json" "iter" "maps" - "path/filepath" "regexp" "slices" "sync" @@ -654,7 +652,7 @@ func (r *PeerRegistry) scheduleSave() { // Must be called with r.mu held (at least RLock). func (r *PeerRegistry) saveNow() error { // Ensure directory exists - dir := filepath.Dir(r.path) + dir := core.PathDir(r.path) if err := fsEnsureDir(dir); err != nil { return core.E("PeerRegistry.saveNow", "failed to create peers directory", err) } @@ -662,10 +660,11 @@ func (r *PeerRegistry) saveNow() error { // Convert to slice for JSON peers := slices.Collect(maps.Values(r.peers)) - data, err := json.MarshalIndent(peers, "", " ") - if err != nil { - return core.E("PeerRegistry.saveNow", "failed to marshal peers", err) + result := core.JSONMarshal(peers) + if !result.OK { + return core.E("PeerRegistry.saveNow", "failed to marshal peers", result.Value.(error)) } + data := result.Value.([]byte) // Use atomic write pattern: write to temp file, then rename tmpPath := r.path + ".tmp" @@ -723,8 +722,9 @@ func (r *PeerRegistry) load() error { } var peers []*Peer - if err := json.Unmarshal([]byte(content), &peers); err != nil { - return core.E("PeerRegistry.load", "failed to unmarshal peers", err) + result := core.JSONUnmarshalString(content, &peers) + if !result.OK { + return core.E("PeerRegistry.load", "failed to unmarshal peers", result.Value.(error)) } r.peers = make(map[string]*Peer) diff --git a/node/protocol.go b/node/protocol.go index f93b2f2..19b55dd 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -1,8 +1,6 @@ package node import ( - "fmt" - core "dappco.re/go/core" ) @@ -13,7 +11,7 @@ type ProtocolError struct { } func (e *ProtocolError) Error() string { - return fmt.Sprintf("remote error (%d): %s", e.Code, e.Message) + return core.Sprintf("remote error (%d): %s", e.Code, e.Message) } // ResponseHandler provides helpers for handling protocol responses. diff --git a/node/transport.go b/node/transport.go index ceb1cb1..44bcc36 100644 --- a/node/transport.go +++ b/node/transport.go @@ -4,8 +4,6 @@ import ( "context" "crypto/tls" "encoding/base64" - "encoding/json" - "fmt" "iter" "maps" "net/http" @@ -457,7 +455,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { // Decode handshake message (not encrypted yet, contains public key) var msg Message - if err := json.Unmarshal(data, &msg); err != nil { + if result := core.JSONUnmarshal(data, &msg); !result.OK { conn.Close() return } @@ -485,7 +483,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { rejectPayload := HandshakeAckPayload{ Identity: *identity, Accepted: false, - Reason: fmt.Sprintf("incompatible protocol version %s, supported: %v", payload.Version, SupportedProtocolVersions), + Reason: core.Sprintf("incompatible protocol version %s, supported: %v", payload.Version, SupportedProtocolVersions), } rejectMsg, _ := NewMessage(MsgHandshakeAck, identity.ID, payload.Identity.ID, rejectPayload) if rejectData, err := MarshalJSON(rejectMsg); err == nil { @@ -660,8 +658,8 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { } var ackMsg Message - if err := json.Unmarshal(ackData, &ackMsg); err != nil { - return core.E("Transport.performHandshake", "unmarshal handshake ack", err) + if result := core.JSONUnmarshal(ackData, &ackMsg); !result.OK { + return core.E("Transport.performHandshake", "unmarshal handshake ack", result.Value.(error)) } if ackMsg.Type != MsgHandshakeAck { @@ -932,8 +930,8 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, // Parse message from JSON var msg Message - if err := json.Unmarshal([]byte(smsgMsg.Body), &msg); err != nil { - return nil, err + if result := core.JSONUnmarshalString(smsgMsg.Body, &msg); !result.OK { + return nil, result.Value.(error) } return &msg, nil diff --git a/node/worker.go b/node/worker.go index 1ea18ea..c5b504c 100644 --- a/node/worker.go +++ b/node/worker.go @@ -2,8 +2,6 @@ package node import ( "encoding/base64" - "encoding/json" - "path/filepath" "time" core "dappco.re/go/core" @@ -331,8 +329,8 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err // Unmarshal into interface{} to pass to ProfileManager var profile any - if err := json.Unmarshal(profileData, &profile); err != nil { - return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", err) + if result := core.JSONUnmarshal(profileData, &profile); !result.OK { + return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", result.Value.(error)) } if err := w.profileManager.SaveProfile(profile); err != nil { @@ -353,8 +351,8 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err case BundleMiner, BundleFull: // Determine installation directory // We use w.DataDir/lethean-desktop/miners/ - minersDir := filepath.Join(w.DataDir, "lethean-desktop", "miners") - installDir := filepath.Join(minersDir, payload.Name) + minersDir := core.JoinPath(w.DataDir, "lethean-desktop", "miners") + installDir := core.JoinPath(minersDir, payload.Name) logging.Info("deploying miner bundle", logging.Fields{ "name": payload.Name, @@ -371,8 +369,8 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err // If the bundle contained a profile config, save it if len(profileData) > 0 && w.profileManager != nil { var profile any - if err := json.Unmarshal(profileData, &profile); err != nil { - logging.Warn("failed to parse profile from miner bundle", logging.Fields{"error": err}) + if result := core.JSONUnmarshal(profileData, &profile); !result.OK { + logging.Warn("failed to parse profile from miner bundle", logging.Fields{"error": result.Value.(error)}) } else { if err := w.profileManager.SaveProfile(profile); err != nil { logging.Warn("failed to save profile from miner bundle", logging.Fields{"error": err}) -- 2.45.3 From d7681bf63741b270b15b7a6b22ebedbe8e4b0616 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 16:26:18 +0000 Subject: [PATCH 03/60] refactor(repo): complete AX v0.8.0 polish pass Co-Authored-By: Virgil --- go.sum | 57 -------- logging/logger.go | 54 +++++++- logging/logger_test.go | 49 +++---- node/ax_test_helpers_test.go | 44 ++++++ node/bench_test.go | 29 ++-- node/bufpool.go | 2 + node/bufpool_test.go | 33 +++-- node/bundle.go | 69 ++++++---- node/bundle_test.go | 84 ++++------- node/controller.go | 4 + node/controller_test.go | 102 ++++++-------- node/dispatcher.go | 10 ++ node/dispatcher_test.go | 24 ++-- node/identity.go | 16 +++ node/identity_test.go | 78 +++-------- node/integration_test.go | 39 +++--- node/levin/connection.go | 4 + node/levin/connection_test.go | 14 +- node/levin/header.go | 6 + node/levin/header_test.go | 18 +-- node/levin/storage.go | 40 ++++++ node/levin/storage_test.go | 38 ++--- node/levin/varint.go | 4 + node/levin/varint_test.go | 32 ++--- node/message.go | 73 +++++++++- node/message_test.go | 39 +++--- node/peer.go | 10 ++ node/peer_test.go | 123 +++++++---------- node/protocol.go | 12 ++ node/protocol_test.go | 15 +- node/transport.go | 22 +++ node/transport_test.go | 55 ++++---- node/worker.go | 10 ++ node/worker_test.go | 253 +++++++++++++--------------------- ueps/packet.go | 6 + ueps/packet_coverage_test.go | 23 ++-- ueps/packet_test.go | 46 +++---- ueps/reader.go | 4 + 38 files changed, 815 insertions(+), 726 deletions(-) create mode 100644 node/ax_test_helpers_test.go diff --git a/go.sum b/go.sum index e06c31a..6783e51 100644 --- a/go.sum +++ b/go.sum @@ -1,99 +1,42 @@ dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= forge.lthn.ai/Snider/Enchantrix v0.0.4/go.mod h1:OGCwuVeZPq3OPe2h6TX/ZbgEjHU6B7owpIBeXQGbSe0= forge.lthn.ai/Snider/Poindexter v0.0.3 h1:cx5wRhuLRKBM8riIZyNVAT2a8rwRhn1dodFBktocsVE= forge.lthn.ai/Snider/Poindexter v0.0.3/go.mod h1:ddzGia98k3HKkR0gl58IDzqz+MmgW2cQJOCNLfuWPpo= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= -github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= -github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging/logger.go b/logging/logger.go index ddb0459..d162458 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -4,14 +4,16 @@ package logging import ( "io" "maps" - "os" "sync" + "syscall" "time" core "dappco.re/go/core" ) // Level represents the severity of a log message. +// +// level := LevelInfo type Level int const ( @@ -42,6 +44,8 @@ func (l Level) String() string { } // Logger provides structured logging with configurable output and level. +// +// logger := New(DefaultConfig()) type Logger struct { mu sync.Mutex output io.Writer @@ -50,6 +54,8 @@ type Logger struct { } // Config holds configuration for creating a new Logger. +// +// cfg := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} type Config struct { Output io.Writer Level Level @@ -57,18 +63,22 @@ type Config struct { } // DefaultConfig returns the default logger configuration. +// +// cfg := DefaultConfig() func DefaultConfig() Config { return Config{ - Output: os.Stderr, + Output: defaultOutput, Level: LevelInfo, Component: "", } } // New creates a new Logger with the given configuration. +// +// logger := New(DefaultConfig()) func New(cfg Config) *Logger { if cfg.Output == nil { - cfg.Output = os.Stderr + cfg.Output = defaultOutput } return &Logger{ output: cfg.Output, @@ -101,8 +111,22 @@ func (l *Logger) GetLevel() Level { } // Fields represents key-value pairs for structured logging. +// +// fields := Fields{"peer_id": "node-1", "attempt": 2} type Fields map[string]any +type stderrWriter struct{} + +func (stderrWriter) Write(p []byte) (int, error) { + written, err := syscall.Write(syscall.Stderr, p) + if err != nil { + return written, core.E("logging.stderrWriter.Write", "failed to write log line", err) + } + return written, nil +} + +var defaultOutput io.Writer = stderrWriter{} + // log writes a log message at the specified level. func (l *Logger) log(level Level, msg string, fields Fields) { l.mu.Lock() @@ -204,6 +228,8 @@ var ( ) // SetGlobal sets the global logger instance. +// +// SetGlobal(New(DefaultConfig())) func SetGlobal(l *Logger) { globalMu.Lock() defer globalMu.Unlock() @@ -211,6 +237,8 @@ func SetGlobal(l *Logger) { } // GetGlobal returns the global logger instance. +// +// logger := GetGlobal() func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() @@ -218,6 +246,8 @@ func GetGlobal() *Logger { } // SetGlobalLevel sets the log level of the global logger. +// +// SetGlobalLevel(LevelDebug) func SetGlobalLevel(level Level) { globalMu.RLock() defer globalMu.RUnlock() @@ -227,46 +257,64 @@ func SetGlobalLevel(level Level) { // Global convenience functions that use the global logger // Debug logs a debug message using the global logger. +// +// Debug("connected", Fields{"peer_id": "node-1"}) func Debug(msg string, fields ...Fields) { GetGlobal().Debug(msg, fields...) } // Info logs an informational message using the global logger. +// +// Info("worker started", Fields{"component": "transport"}) func Info(msg string, fields ...Fields) { GetGlobal().Info(msg, fields...) } // Warn logs a warning message using the global logger. +// +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(msg string, fields ...Fields) { GetGlobal().Warn(msg, fields...) } // Error logs an error message using the global logger. +// +// Error("send failed", Fields{"peer_id": "node-1"}) func Error(msg string, fields ...Fields) { GetGlobal().Error(msg, fields...) } // Debugf logs a formatted debug message using the global logger. +// +// Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { GetGlobal().Debugf(format, args...) } // Infof logs a formatted informational message using the global logger. +// +// Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { GetGlobal().Infof(format, args...) } // Warnf logs a formatted warning message using the global logger. +// +// Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { GetGlobal().Warnf(format, args...) } // Errorf logs a formatted error message using the global logger. +// +// Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { GetGlobal().Errorf(format, args...) } // ParseLevel parses a string into a log level. +// +// level, err := ParseLevel("warn") func ParseLevel(s string) (Level, error) { switch core.Upper(s) { case "DEBUG": diff --git a/logging/logger_test.go b/logging/logger_test.go index 5fa5163..38f8cb9 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -2,11 +2,12 @@ package logging import ( "bytes" - "strings" "testing" + + core "dappco.re/go/core" ) -func TestLoggerLevels(t *testing.T) { +func TestLogger_Levels_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -21,29 +22,29 @@ func TestLoggerLevels(t *testing.T) { // Info should appear logger.Info("info message") - if !strings.Contains(buf.String(), "[INFO]") { + if !core.Contains(buf.String(), "[INFO]") { t.Error("Info message should appear") } - if !strings.Contains(buf.String(), "info message") { + if !core.Contains(buf.String(), "info message") { t.Error("Info message content should appear") } buf.Reset() // Warn should appear logger.Warn("warn message") - if !strings.Contains(buf.String(), "[WARN]") { + if !core.Contains(buf.String(), "[WARN]") { t.Error("Warn message should appear") } buf.Reset() // Error should appear logger.Error("error message") - if !strings.Contains(buf.String(), "[ERROR]") { + if !core.Contains(buf.String(), "[ERROR]") { t.Error("Error message should appear") } } -func TestLoggerDebugLevel(t *testing.T) { +func TestLogger_DebugLevel_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -51,12 +52,12 @@ func TestLoggerDebugLevel(t *testing.T) { }) logger.Debug("debug message") - if !strings.Contains(buf.String(), "[DEBUG]") { + if !core.Contains(buf.String(), "[DEBUG]") { t.Error("Debug message should appear at Debug level") } } -func TestLoggerWithFields(t *testing.T) { +func TestLogger_WithFields_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -66,15 +67,15 @@ func TestLoggerWithFields(t *testing.T) { logger.Info("test message", Fields{"key": "value", "num": 42}) output := buf.String() - if !strings.Contains(output, "key=value") { + if !core.Contains(output, "key=value") { t.Error("Field key=value should appear") } - if !strings.Contains(output, "num=42") { + if !core.Contains(output, "num=42") { t.Error("Field num=42 should appear") } } -func TestLoggerWithComponent(t *testing.T) { +func TestLogger_WithComponent_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -85,12 +86,12 @@ func TestLoggerWithComponent(t *testing.T) { logger.Info("test message") output := buf.String() - if !strings.Contains(output, "[TestComponent]") { + if !core.Contains(output, "[TestComponent]") { t.Error("Component name should appear in log") } } -func TestLoggerDerivedComponent(t *testing.T) { +func TestLogger_DerivedComponent_Good(t *testing.T) { var buf bytes.Buffer parent := New(Config{ Output: &buf, @@ -101,12 +102,12 @@ func TestLoggerDerivedComponent(t *testing.T) { child.Info("child message") output := buf.String() - if !strings.Contains(output, "[ChildComponent]") { + if !core.Contains(output, "[ChildComponent]") { t.Error("Derived component name should appear") } } -func TestLoggerFormatted(t *testing.T) { +func TestLogger_Formatted_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -116,12 +117,12 @@ func TestLoggerFormatted(t *testing.T) { logger.Infof("formatted %s %d", "string", 123) output := buf.String() - if !strings.Contains(output, "formatted string 123") { + if !core.Contains(output, "formatted string 123") { t.Errorf("Formatted message should appear, got: %s", output) } } -func TestSetLevel(t *testing.T) { +func TestLogger_SetLevel_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -137,7 +138,7 @@ func TestSetLevel(t *testing.T) { // Change to Info level logger.SetLevel(LevelInfo) logger.Info("should appear now") - if !strings.Contains(buf.String(), "should appear now") { + if !core.Contains(buf.String(), "should appear now") { t.Error("Info should appear after level change") } @@ -147,7 +148,7 @@ func TestSetLevel(t *testing.T) { } } -func TestParseLevel(t *testing.T) { +func TestLogger_ParseLevel_Good(t *testing.T) { tests := []struct { input string expected Level @@ -180,7 +181,7 @@ func TestParseLevel(t *testing.T) { } } -func TestGlobalLogger(t *testing.T) { +func TestLogger_GlobalLogger_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -190,7 +191,7 @@ func TestGlobalLogger(t *testing.T) { SetGlobal(logger) Info("global test") - if !strings.Contains(buf.String(), "global test") { + if !core.Contains(buf.String(), "global test") { t.Error("Global logger should write message") } @@ -205,7 +206,7 @@ func TestGlobalLogger(t *testing.T) { SetGlobal(New(DefaultConfig())) } -func TestLevelString(t *testing.T) { +func TestLogger_LevelString_Good(t *testing.T) { tests := []struct { level Level expected string @@ -224,7 +225,7 @@ func TestLevelString(t *testing.T) { } } -func TestMergeFields(t *testing.T) { +func TestLogger_MergeFields_Good(t *testing.T) { // Empty fields result := mergeFields(nil) if result != nil { diff --git a/node/ax_test_helpers_test.go b/node/ax_test_helpers_test.go new file mode 100644 index 0000000..5b2051e --- /dev/null +++ b/node/ax_test_helpers_test.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package node + +import ( + "io/fs" + "testing" + + core "dappco.re/go/core" + "github.com/stretchr/testify/require" +) + +func testJoinPath(parts ...string) string { + return core.JoinPath(parts...) +} + +func testNodeManagerPaths(dir string) (string, string) { + return testJoinPath(dir, "private.key"), testJoinPath(dir, "node.json") +} + +func testWriteFile(t *testing.T, path string, content []byte, mode fs.FileMode) { + t.Helper() + require.NoError(t, fsResultErr(localFS.WriteMode(path, string(content), mode))) +} + +func testReadFile(t *testing.T, path string) []byte { + t.Helper() + content, err := fsRead(path) + require.NoError(t, err) + return []byte(content) +} + +func testJSONMarshal(t *testing.T, v any) []byte { + t.Helper() + result := core.JSONMarshal(v) + require.True(t, result.OK, "marshal should succeed: %v", result.Value) + return result.Value.([]byte) +} + +func testJSONUnmarshal(t *testing.T, data []byte, target any) { + t.Helper() + result := core.JSONUnmarshal(data, target) + require.True(t, result.OK, "unmarshal should succeed: %v", result.Value) +} diff --git a/node/bench_test.go b/node/bench_test.go index 7123797..072404f 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -2,11 +2,10 @@ package node import ( "encoding/base64" - "encoding/json" - "path/filepath" "testing" "time" + core "dappco.re/go/core" "forge.lthn.ai/Snider/Borg/pkg/smsg" ) @@ -16,10 +15,7 @@ func BenchmarkIdentityGenerate(b *testing.B) { b.ReportAllocs() for b.Loop() { dir := b.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { b.Fatalf("create node manager: %v", err) } @@ -34,10 +30,10 @@ func BenchmarkDeriveSharedSecret(b *testing.B) { dir1 := b.TempDir() dir2 := b.TempDir() - nm1, _ := NewNodeManagerWithPaths(filepath.Join(dir1, "k"), filepath.Join(dir1, "n")) + nm1, _ := NewNodeManagerWithPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) nm1.GenerateIdentity("node1", RoleDual) - nm2, _ := NewNodeManagerWithPaths(filepath.Join(dir2, "k"), filepath.Join(dir2, "n")) + nm2, _ := NewNodeManagerWithPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) peerPubKey := nm2.GetIdentity().PublicKey @@ -88,8 +84,8 @@ func BenchmarkMessageSerialise(b *testing.B) { } var restored Message - if err := json.Unmarshal(data, &restored); err != nil { - b.Fatalf("unmarshal message: %v", err) + if result := core.JSONUnmarshal(data, &restored); !result.OK { + b.Fatalf("unmarshal message: %v", result.Value) } } } @@ -136,9 +132,8 @@ func BenchmarkMarshalJSON(b *testing.B) { b.Run("Stdlib", func(b *testing.B) { b.ReportAllocs() for b.Loop() { - _, err := json.Marshal(data) - if err != nil { - b.Fatal(err) + if result := core.JSONMarshal(data); !result.OK { + b.Fatal(result.Value) } } }) @@ -150,10 +145,10 @@ func BenchmarkSMSGEncryptDecrypt(b *testing.B) { dir1 := b.TempDir() dir2 := b.TempDir() - nm1, _ := NewNodeManagerWithPaths(filepath.Join(dir1, "k"), filepath.Join(dir1, "n")) + nm1, _ := NewNodeManagerWithPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) nm1.GenerateIdentity("node1", RoleDual) - nm2, _ := NewNodeManagerWithPaths(filepath.Join(dir2, "k"), filepath.Join(dir2, "n")) + nm2, _ := NewNodeManagerWithPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) sharedSecret, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) @@ -202,7 +197,7 @@ func BenchmarkChallengeSignVerify(b *testing.B) { // BenchmarkPeerScoring measures KD-tree rebuild and peer selection. func BenchmarkPeerScoring(b *testing.B) { dir := b.TempDir() - reg, err := NewPeerRegistryWithPath(filepath.Join(dir, "peers.json")) + reg, err := NewPeerRegistryWithPath(testJoinPath(dir, "peers.json")) if err != nil { b.Fatalf("create registry: %v", err) } @@ -211,7 +206,7 @@ func BenchmarkPeerScoring(b *testing.B) { // Add 50 peers with varied metrics for i := range 50 { peer := &Peer{ - ID: filepath.Join("peer", string(rune('A'+i%26)), string(rune('0'+i/26))), + ID: testJoinPath("peer", string(rune('A'+i%26)), string(rune('0'+i/26))), Name: "peer", PingMS: float64(i*10 + 5), Hops: i%5 + 1, diff --git a/node/bufpool.go b/node/bufpool.go index e7244c2..49d98ac 100644 --- a/node/bufpool.go +++ b/node/bufpool.go @@ -33,6 +33,8 @@ func putBuffer(buf *bytes.Buffer) { // MarshalJSON encodes a value to JSON using Core's JSON primitive and then // restores the historical no-EscapeHTML behaviour expected by the node package. // Returns a copy of the encoded bytes (safe to use after the function returns). +// +// data, err := MarshalJSON(v) func MarshalJSON(v any) ([]byte, error) { encoded := core.JSONMarshal(v) if !encoded.OK { diff --git a/node/bufpool_test.go b/node/bufpool_test.go index cd0c786..548761c 100644 --- a/node/bufpool_test.go +++ b/node/bufpool_test.go @@ -2,17 +2,17 @@ package node import ( "bytes" - "encoding/json" "sync" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- bufpool.go tests --- -func TestGetBuffer_ReturnsResetBuffer(t *testing.T) { +func TestBufpool_GetBuffer_ReturnsResetBuffer_Good(t *testing.T) { t.Run("buffer is initially empty", func(t *testing.T) { buf := getBuffer() defer putBuffer(buf) @@ -33,7 +33,7 @@ func TestGetBuffer_ReturnsResetBuffer(t *testing.T) { }) } -func TestPutBuffer_DiscardsOversizedBuffers(t *testing.T) { +func TestBufpool_PutBuffer_DiscardsOversizedBuffers_Good(t *testing.T) { t.Run("buffer at 64KB limit is pooled", func(t *testing.T) { buf := getBuffer() buf.Grow(65536) @@ -59,7 +59,7 @@ func TestPutBuffer_DiscardsOversizedBuffers(t *testing.T) { }) } -func TestBufPool_BufferIndependence(t *testing.T) { +func TestBufpool_BufPool_BufferIndependence_Good(t *testing.T) { buf1 := getBuffer() buf2 := getBuffer() @@ -77,7 +77,7 @@ func TestBufPool_BufferIndependence(t *testing.T) { putBuffer(buf2) } -func TestMarshalJSON_BasicTypes(t *testing.T) { +func TestBufpool_MarshalJSON_BasicTypes_Good(t *testing.T) { tests := []struct { name string input any @@ -121,8 +121,7 @@ func TestMarshalJSON_BasicTypes(t *testing.T) { got, err := MarshalJSON(tt.input) require.NoError(t, err) - expected, err := json.Marshal(tt.input) - require.NoError(t, err) + expected := testJSONMarshal(t, tt.input) assert.JSONEq(t, string(expected), string(got), "MarshalJSON output should match json.Marshal") @@ -130,7 +129,7 @@ func TestMarshalJSON_BasicTypes(t *testing.T) { } } -func TestMarshalJSON_NoTrailingNewline(t *testing.T) { +func TestBufpool_MarshalJSON_NoTrailingNewline_Good(t *testing.T) { data, err := MarshalJSON(map[string]string{"key": "value"}) require.NoError(t, err) @@ -138,7 +137,7 @@ func TestMarshalJSON_NoTrailingNewline(t *testing.T) { "MarshalJSON should strip the trailing newline added by json.Encoder") } -func TestMarshalJSON_HTMLEscaping(t *testing.T) { +func TestBufpool_MarshalJSON_HTMLEscaping_Good(t *testing.T) { input := map[string]string{"html": ""} data, err := MarshalJSON(input) require.NoError(t, err) @@ -147,7 +146,7 @@ func TestMarshalJSON_HTMLEscaping(t *testing.T) { "HTML characters should not be escaped when EscapeHTML is false") } -func TestMarshalJSON_ReturnsCopy(t *testing.T) { +func TestBufpool_MarshalJSON_ReturnsCopy_Good(t *testing.T) { data1, err := MarshalJSON("first") require.NoError(t, err) @@ -162,7 +161,7 @@ func TestMarshalJSON_ReturnsCopy(t *testing.T) { "returned slice should be a copy and not be mutated by subsequent calls") } -func TestMarshalJSON_ReturnsIndependentCopy(t *testing.T) { +func TestBufpool_MarshalJSON_ReturnsIndependentCopy_Good(t *testing.T) { data1, err := MarshalJSON(map[string]string{"first": "call"}) require.NoError(t, err) @@ -175,13 +174,13 @@ func TestMarshalJSON_ReturnsIndependentCopy(t *testing.T) { "second result should contain its own data") } -func TestMarshalJSON_InvalidValue(t *testing.T) { +func TestBufpool_MarshalJSON_InvalidValue_Bad(t *testing.T) { ch := make(chan int) _, err := MarshalJSON(ch) assert.Error(t, err, "marshalling an unserialisable type should return an error") } -func TestBufferPool_ConcurrentAccess(t *testing.T) { +func TestBufpool_BufferPool_ConcurrentAccess_Ugly(t *testing.T) { const goroutines = 100 const iterations = 50 @@ -206,7 +205,7 @@ func TestBufferPool_ConcurrentAccess(t *testing.T) { wg.Wait() } -func TestMarshalJSON_ConcurrentSafety(t *testing.T) { +func TestBufpool_MarshalJSON_ConcurrentSafety_Ugly(t *testing.T) { const goroutines = 50 var wg sync.WaitGroup @@ -223,8 +222,8 @@ func TestMarshalJSON_ConcurrentSafety(t *testing.T) { if err == nil { var parsed PingPayload - err = json.Unmarshal(data, &parsed) - if err != nil { + if result := core.JSONUnmarshal(data, &parsed); !result.OK { + err = result.Value.(error) errs[idx] = err return } @@ -242,7 +241,7 @@ func TestMarshalJSON_ConcurrentSafety(t *testing.T) { } } -func TestBufferPool_ReuseAfterReset(t *testing.T) { +func TestBufpool_BufferPool_ReuseAfterReset_Ugly(t *testing.T) { buf := getBuffer() buf.Write(make([]byte, 4096)) putBuffer(buf) diff --git a/node/bundle.go b/node/bundle.go index f3866c8..8b42afa 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -6,7 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "io" - "os" + "io/fs" core "dappco.re/go/core" @@ -15,15 +15,22 @@ import ( ) // BundleType defines the type of deployment bundle. +// +// bundleType := BundleProfile type BundleType string const ( - BundleProfile BundleType = "profile" // Just config/profile JSON - BundleMiner BundleType = "miner" // Miner binary + config - BundleFull BundleType = "full" // Everything (miner + profiles + config) + // BundleProfile contains a profile JSON payload. + BundleProfile BundleType = "profile" + // BundleMiner contains a miner binary and optional profile data. + BundleMiner BundleType = "miner" + // BundleFull contains the full deployment payload. + BundleFull BundleType = "full" ) // Bundle represents a deployment bundle for P2P transfer. +// +// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} type Bundle struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -32,6 +39,8 @@ type Bundle struct { } // BundleManifest describes the contents of a bundle. +// +// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} type BundleManifest struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -42,6 +51,8 @@ type BundleManifest struct { } // CreateProfileBundle creates an encrypted bundle containing a mining profile. +// +// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config t, err := tim.New() @@ -68,6 +79,8 @@ func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bun } // CreateProfileBundleUnencrypted creates a plain JSON bundle (for testing or trusted networks). +// +// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error) { checksum := calculateChecksum(profileJSON) @@ -80,6 +93,8 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e } // CreateMinerBundle creates an encrypted bundle containing a miner binary and optional profile. +// +// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary minerContent, err := fsRead(minerPath) @@ -130,6 +145,8 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo } // ExtractProfileBundle decrypts and extracts a profile bundle. +// +// profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -151,6 +168,8 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { } // ExtractMinerBundle decrypts and extracts a miner bundle, returning the miner path and profile. +// +// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -179,6 +198,8 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string } // VerifyBundle checks if a bundle's checksum is valid. +// +// ok := VerifyBundle(bundle) func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } @@ -251,14 +272,18 @@ func createTarball(files map[string][]byte) ([]byte, error) { func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir := destDir + pathSeparator := core.Env("DS") + if pathSeparator == "" { + pathSeparator = "/" + } if !core.PathIsAbs(absDestDir) { - cwd, err := os.Getwd() - if err != nil { - return "", core.E("extractTarball", "failed to resolve destination directory", err) + cwd := core.Env("DIR_CWD") + if cwd == "" { + return "", core.E("extractTarball", "failed to resolve destination directory", nil) } - absDestDir = core.CleanPath(core.Concat(cwd, string(os.PathSeparator), absDestDir), string(os.PathSeparator)) + absDestDir = core.CleanPath(core.Concat(cwd, pathSeparator, absDestDir), pathSeparator) } else { - absDestDir = core.CleanPath(absDestDir, string(os.PathSeparator)) + absDestDir = core.CleanPath(absDestDir, pathSeparator) } if err := fsEnsureDir(absDestDir); err != nil { @@ -291,11 +316,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) { } // Build the full path and verify it's within destDir - fullPath := core.CleanPath(core.Concat(absDestDir, string(os.PathSeparator), cleanName), string(os.PathSeparator)) + fullPath := core.CleanPath(core.Concat(absDestDir, pathSeparator, cleanName), pathSeparator) // Final security check: ensure the path is still within destDir - allowedPrefix := core.Concat(absDestDir, string(os.PathSeparator)) - if absDestDir == string(os.PathSeparator) { + allowedPrefix := core.Concat(absDestDir, pathSeparator) + if absDestDir == pathSeparator { allowedPrefix = absDestDir } if !core.HasPrefix(fullPath, allowedPrefix) && fullPath != absDestDir { @@ -313,26 +338,20 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return "", err } - // os.OpenFile is used deliberately here instead of core.Fs.Create/Write - // because the helper writes with fixed default permissions and we need to preserve - // the tar header's mode bits — executable binaries require 0755. - f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)) - if err != nil { - return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) - } - // Limit file size to prevent decompression bombs (100MB max per file) const maxFileSize int64 = 100 * 1024 * 1024 limitedReader := io.LimitReader(tr, maxFileSize+1) - written, err := io.Copy(f, limitedReader) - f.Close() + content, err := io.ReadAll(limitedReader) if err != nil { return "", core.E("extractTarball", "failed to write file "+hdr.Name, err) } - if written > maxFileSize { + if int64(len(content)) > maxFileSize { fsDelete(fullPath) return "", core.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) } + if err := fsResultErr(localFS.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { + return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) + } // Track first executable if hdr.Mode&0111 != 0 && firstExecutable == "" { @@ -349,6 +368,8 @@ func extractTarball(tarData []byte, destDir string) (string, error) { } // StreamBundle writes a bundle to a writer (for large transfers). +// +// err := StreamBundle(bundle, writer) func StreamBundle(bundle *Bundle, w io.Writer) error { result := core.JSONMarshal(bundle) if !result.OK { @@ -359,6 +380,8 @@ func StreamBundle(bundle *Bundle, w io.Writer) error { } // ReadBundle reads a bundle from a reader. +// +// bundle, err := ReadBundle(reader) func ReadBundle(r io.Reader) (*Bundle, error) { var buf bytes.Buffer if _, err := io.Copy(&buf, r); err != nil { diff --git a/node/bundle_test.go b/node/bundle_test.go index 80c2ed3..8d04966 100644 --- a/node/bundle_test.go +++ b/node/bundle_test.go @@ -3,12 +3,10 @@ package node import ( "archive/tar" "bytes" - "os" - "path/filepath" "testing" ) -func TestCreateProfileBundleUnencrypted(t *testing.T) { +func TestBundle_CreateProfileBundleUnencrypted_Good(t *testing.T) { profileJSON := []byte(`{"name":"test-profile","minerType":"xmrig","config":{}}`) bundle, err := CreateProfileBundleUnencrypted(profileJSON, "test-profile") @@ -33,7 +31,7 @@ func TestCreateProfileBundleUnencrypted(t *testing.T) { } } -func TestVerifyBundle(t *testing.T) { +func TestBundle_VerifyBundle_Good(t *testing.T) { t.Run("ValidChecksum", func(t *testing.T) { bundle, _ := CreateProfileBundleUnencrypted([]byte(`{"test":"data"}`), "test") @@ -61,7 +59,7 @@ func TestVerifyBundle(t *testing.T) { }) } -func TestCreateProfileBundle(t *testing.T) { +func TestBundle_CreateProfileBundle_Good(t *testing.T) { profileJSON := []byte(`{"name":"encrypted-profile","minerType":"xmrig"}`) password := "test-password-123" @@ -90,7 +88,7 @@ func TestCreateProfileBundle(t *testing.T) { } } -func TestExtractProfileBundle(t *testing.T) { +func TestBundle_ExtractProfileBundle_Good(t *testing.T) { t.Run("UnencryptedBundle", func(t *testing.T) { originalJSON := []byte(`{"name":"plain","config":{}}`) bundle, _ := CreateProfileBundleUnencrypted(originalJSON, "plain") @@ -142,7 +140,7 @@ func TestExtractProfileBundle(t *testing.T) { }) } -func TestTarballFunctions(t *testing.T) { +func TestBundle_TarballFunctions_Good(t *testing.T) { t.Run("CreateAndExtractTarball", func(t *testing.T) { files := map[string][]byte{ "file1.txt": []byte("content of file 1"), @@ -160,8 +158,7 @@ func TestTarballFunctions(t *testing.T) { } // Extract to temp directory - tmpDir, _ := os.MkdirTemp("", "tarball-test") - defer os.RemoveAll(tmpDir) + tmpDir := t.TempDir() firstExec, err := extractTarball(tarData, tmpDir) if err != nil { @@ -170,12 +167,7 @@ func TestTarballFunctions(t *testing.T) { // Check files exist for name, content := range files { - path := filepath.Join(tmpDir, name) - data, err := os.ReadFile(path) - if err != nil { - t.Errorf("failed to read extracted file %s: %v", name, err) - continue - } + data := testReadFile(t, testJoinPath(tmpDir, name)) if !bytes.Equal(data, content) { t.Errorf("content mismatch for %s", name) @@ -189,7 +181,7 @@ func TestTarballFunctions(t *testing.T) { }) } -func TestStreamAndReadBundle(t *testing.T) { +func TestBundle_StreamAndReadBundle_Good(t *testing.T) { original, _ := CreateProfileBundleUnencrypted([]byte(`{"streaming":"test"}`), "stream-test") // Stream to buffer @@ -218,7 +210,7 @@ func TestStreamAndReadBundle(t *testing.T) { } } -func TestCalculateChecksum(t *testing.T) { +func TestBundle_CalculateChecksum_Good(t *testing.T) { t.Run("Deterministic", func(t *testing.T) { data := []byte("test data for checksum") @@ -256,7 +248,7 @@ func TestCalculateChecksum(t *testing.T) { }) } -func TestIsJSON(t *testing.T) { +func TestBundle_IsJSON_Good(t *testing.T) { tests := []struct { data []byte expected bool @@ -279,7 +271,7 @@ func TestIsJSON(t *testing.T) { } } -func TestBundleTypes(t *testing.T) { +func TestBundle_Types_Good(t *testing.T) { types := []BundleType{ BundleProfile, BundleMiner, @@ -295,16 +287,11 @@ func TestBundleTypes(t *testing.T) { } } -func TestCreateMinerBundle(t *testing.T) { +func TestBundle_CreateMinerBundle_Good(t *testing.T) { // Create a temp "miner binary" - tmpDir, _ := os.MkdirTemp("", "miner-bundle-test") - defer os.RemoveAll(tmpDir) - - minerPath := filepath.Join(tmpDir, "test-miner") - err := os.WriteFile(minerPath, []byte("fake miner binary content"), 0755) - if err != nil { - t.Fatalf("failed to create test miner: %v", err) - } + tmpDir := t.TempDir() + minerPath := testJoinPath(tmpDir, "test-miner") + testWriteFile(t, minerPath, []byte("fake miner binary content"), 0o755) profileJSON := []byte(`{"profile":"data"}`) password := "miner-password" @@ -323,8 +310,7 @@ func TestCreateMinerBundle(t *testing.T) { } // Extract and verify - extractDir, _ := os.MkdirTemp("", "miner-extract-test") - defer os.RemoveAll(extractDir) + extractDir := t.TempDir() extractedPath, extractedProfile, err := ExtractMinerBundle(bundle, password, extractDir) if err != nil { @@ -341,10 +327,7 @@ func TestCreateMinerBundle(t *testing.T) { // If we got an extracted path, verify its content if extractedPath != "" { - minerData, err := os.ReadFile(extractedPath) - if err != nil { - t.Fatalf("failed to read extracted miner: %v", err) - } + minerData := testReadFile(t, extractedPath) if string(minerData) != "fake miner binary content" { t.Error("miner content mismatch") @@ -354,7 +337,7 @@ func TestCreateMinerBundle(t *testing.T) { // --- Additional coverage tests for bundle.go --- -func TestExtractTarball_PathTraversal(t *testing.T) { +func TestBundle_ExtractTarball_PathTraversal_Bad(t *testing.T) { t.Run("AbsolutePath", func(t *testing.T) { // Create a tarball with an absolute path entry tarData, err := createTarballWithCustomName("/etc/passwd", []byte("malicious")) @@ -446,8 +429,8 @@ func TestExtractTarball_PathTraversal(t *testing.T) { } // Verify symlink was not created - linkPath := filepath.Join(tmpDir, "link") - if _, statErr := os.Lstat(linkPath); !os.IsNotExist(statErr) { + linkPath := testJoinPath(tmpDir, "link") + if fsExists(linkPath) { t.Error("symlink should not be created") } }) @@ -481,10 +464,7 @@ func TestExtractTarball_PathTraversal(t *testing.T) { } // Verify directory and file exist - data, err := os.ReadFile(filepath.Join(tmpDir, "mydir", "file.txt")) - if err != nil { - t.Fatalf("failed to read extracted file: %v", err) - } + data := testReadFile(t, testJoinPath(tmpDir, "mydir", "file.txt")) if !bytes.Equal(data, content) { t.Error("content mismatch") } @@ -531,7 +511,7 @@ func createTarballWithSymlink(name, target string) ([]byte, error) { return buf.Bytes(), nil } -func TestExtractMinerBundle_ChecksumMismatch(t *testing.T) { +func TestBundle_ExtractMinerBundle_ChecksumMismatch_Bad(t *testing.T) { bundle := &Bundle{ Type: BundleMiner, Name: "bad-bundle", @@ -545,17 +525,17 @@ func TestExtractMinerBundle_ChecksumMismatch(t *testing.T) { } } -func TestCreateMinerBundle_NonExistentFile(t *testing.T) { +func TestBundle_CreateMinerBundle_NonExistentFile_Bad(t *testing.T) { _, err := CreateMinerBundle("/non/existent/miner", nil, "test", "password") if err == nil { t.Error("expected error for non-existent miner file") } } -func TestCreateMinerBundle_NilProfile(t *testing.T) { +func TestBundle_CreateMinerBundle_NilProfile_Ugly(t *testing.T) { tmpDir := t.TempDir() - minerPath := filepath.Join(tmpDir, "miner") - os.WriteFile(minerPath, []byte("binary"), 0755) + minerPath := testJoinPath(tmpDir, "miner") + testWriteFile(t, minerPath, []byte("binary"), 0o755) bundle, err := CreateMinerBundle(minerPath, nil, "nil-profile", "pass") if err != nil { @@ -566,7 +546,7 @@ func TestCreateMinerBundle_NilProfile(t *testing.T) { } } -func TestReadBundle_InvalidJSON(t *testing.T) { +func TestBundle_ReadBundle_InvalidJSON_Bad(t *testing.T) { reader := bytes.NewReader([]byte("not json")) _, err := ReadBundle(reader) if err == nil { @@ -574,7 +554,7 @@ func TestReadBundle_InvalidJSON(t *testing.T) { } } -func TestStreamBundle_EmptyBundle(t *testing.T) { +func TestBundle_StreamBundle_EmptyBundle_Ugly(t *testing.T) { bundle := &Bundle{ Type: BundleProfile, Name: "empty", @@ -598,7 +578,7 @@ func TestStreamBundle_EmptyBundle(t *testing.T) { } } -func TestCreateTarball_MultipleDirs(t *testing.T) { +func TestBundle_CreateTarball_MultipleDirs_Good(t *testing.T) { files := map[string][]byte{ "dir1/file1.txt": []byte("content1"), "dir2/file2.txt": []byte("content2"), @@ -616,11 +596,7 @@ func TestCreateTarball_MultipleDirs(t *testing.T) { } for name, content := range files { - data, err := os.ReadFile(filepath.Join(tmpDir, name)) - if err != nil { - t.Errorf("failed to read %s: %v", name, err) - continue - } + data := testReadFile(t, testJoinPath(tmpDir, name)) if !bytes.Equal(data, content) { t.Errorf("content mismatch for %s", name) } diff --git a/node/controller.go b/node/controller.go index cf7f21b..5724b9a 100644 --- a/node/controller.go +++ b/node/controller.go @@ -11,6 +11,8 @@ import ( ) // Controller manages remote peer operations from a controller node. +// +// controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { node *NodeManager peers *PeerRegistry @@ -22,6 +24,8 @@ type Controller struct { } // NewController creates a new Controller instance. +// +// controller := NewController(nodeManager, peerRegistry, transport) func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller { c := &Controller{ node: node, diff --git a/node/controller_test.go b/node/controller_test.go index ee9a383..937bff4 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -1,17 +1,15 @@ package node import ( - "encoding/json" - "fmt" "net/http" "net/http/httptest" "net/url" - "path/filepath" "sync" "sync/atomic" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -75,7 +73,7 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { // --- Controller Tests --- -func TestController_RequestResponseCorrelation(t *testing.T) { +func TestController_RequestResponseCorrelation_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -86,7 +84,7 @@ func TestController_RequestResponseCorrelation(t *testing.T) { assert.Greater(t, rtt, 0.0, "RTT should be positive") } -func TestController_RequestTimeout(t *testing.T) { +func TestController_RequestTimeout_Bad(t *testing.T) { tp := setupTestTransportPair(t) // Register a handler on the server that deliberately ignores all messages, @@ -117,7 +115,7 @@ func TestController_RequestTimeout(t *testing.T) { assert.Less(t, elapsed, 1*time.Second, "should return quickly after the deadline") } -func TestController_AutoConnect(t *testing.T) { +func TestController_AutoConnect_Good(t *testing.T) { tp := setupTestTransportPair(t) // Register worker on the server side. @@ -149,7 +147,7 @@ func TestController_AutoConnect(t *testing.T) { assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection after auto-connect") } -func TestController_GetAllStats(t *testing.T) { +func TestController_GetAllStats_Good(t *testing.T) { // Controller node with connections to two independent worker servers. controllerNM := testNode(t, "controller", RoleController) controllerReg := testRegistry(t) @@ -194,7 +192,7 @@ func TestController_GetAllStats(t *testing.T) { } } -func TestController_PingPeerRTT(t *testing.T) { +func TestController_PingPeerRTT_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -217,7 +215,7 @@ func TestController_PingPeerRTT(t *testing.T) { assert.Greater(t, peerAfter.PingMS, 0.0, "PingMS should be positive") } -func TestController_ConcurrentRequests(t *testing.T) { +func TestController_ConcurrentRequests_Ugly(t *testing.T) { // Multiple goroutines send pings to different peers simultaneously. // Verify correct correlation — no cross-talk between responses. controllerNM := testNode(t, "controller", RoleController) @@ -271,7 +269,7 @@ func TestController_ConcurrentRequests(t *testing.T) { } } -func TestController_DeadPeerCleanup(t *testing.T) { +func TestController_DeadPeerCleanup_Good(t *testing.T) { tp := setupTestTransportPair(t) // Server deliberately ignores all messages. @@ -307,7 +305,7 @@ func TestController_DeadPeerCleanup(t *testing.T) { // --- Additional edge-case tests --- -func TestController_MultipleSequentialPings(t *testing.T) { +func TestController_MultipleSequentialPings_Good(t *testing.T) { // Ensures sequential requests to the same peer are correctly correlated. controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -319,7 +317,7 @@ func TestController_MultipleSequentialPings(t *testing.T) { } } -func TestController_ConcurrentRequestsSamePeer(t *testing.T) { +func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { // Multiple goroutines sending requests to the SAME peer simultaneously. // Tests concurrent pending-map insertions/deletions under contention. controller, _, tp := setupControllerPair(t) @@ -343,7 +341,7 @@ func TestController_ConcurrentRequestsSamePeer(t *testing.T) { "all concurrent requests to the same peer should succeed") } -func TestController_GetRemoteStats(t *testing.T) { +func TestController_GetRemoteStats_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -357,7 +355,7 @@ func TestController_GetRemoteStats(t *testing.T) { assert.GreaterOrEqual(t, stats.Uptime, int64(0), "uptime should be non-negative") } -func TestController_ConnectToPeerUnknown(t *testing.T) { +func TestController_ConnectToPeerUnknown_Bad(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -366,7 +364,7 @@ func TestController_ConnectToPeerUnknown(t *testing.T) { assert.Contains(t, err.Error(), "not found") } -func TestController_DisconnectFromPeer(t *testing.T) { +func TestController_DisconnectFromPeer_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -376,7 +374,7 @@ func TestController_DisconnectFromPeer(t *testing.T) { require.NoError(t, err, "DisconnectFromPeer should succeed") } -func TestController_DisconnectFromPeerNotConnected(t *testing.T) { +func TestController_DisconnectFromPeerNotConnected_Bad(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -385,7 +383,7 @@ func TestController_DisconnectFromPeerNotConnected(t *testing.T) { assert.Contains(t, err.Error(), "not connected") } -func TestController_SendRequestPeerNotFound(t *testing.T) { +func TestController_SendRequestPeerNotFound_Bad(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -475,7 +473,7 @@ func (m *mockMinerManagerFull) StopMiner(name string) error { defer m.mu.Unlock() if _, exists := m.miners[name]; !exists { - return fmt.Errorf("miner %s not found", name) + return core.E("mockMinerManagerFull.StopMiner", "miner "+name+" not found", nil) } delete(m.miners, name) return nil @@ -498,7 +496,7 @@ func (m *mockMinerManagerFull) GetMiner(name string) (MinerInstance, error) { miner, exists := m.miners[name] if !exists { - return nil, fmt.Errorf("miner %s not found", name) + return nil, core.E("mockMinerManagerFull.GetMiner", "miner "+name+" not found", nil) } return miner, nil } @@ -521,25 +519,25 @@ func (m *mockMinerFull) GetConsoleHistory(lines int) []string { return m.consoleHistory[:lines] } -func TestController_StartRemoteMiner(t *testing.T) { +func TestController_StartRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID - configOverride := json.RawMessage(`{"pool":"pool.example.com:3333"}`) + configOverride := RawMessage(`{"pool":"pool.example.com:3333"}`) err := controller.StartRemoteMiner(serverID, "xmrig", "profile-1", configOverride) require.NoError(t, err, "StartRemoteMiner should succeed") } -func TestController_StartRemoteMiner_WithConfig(t *testing.T) { +func TestController_StartRemoteMiner_WithConfig_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID - configOverride := json.RawMessage(`{"pool":"custom-pool:3333","threads":4}`) + configOverride := RawMessage(`{"pool":"custom-pool:3333","threads":4}`) err := controller.StartRemoteMiner(serverID, "xmrig", "", configOverride) require.NoError(t, err, "StartRemoteMiner with config override should succeed") } -func TestController_StartRemoteMiner_EmptyType(t *testing.T) { +func TestController_StartRemoteMiner_EmptyType_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -548,14 +546,12 @@ func TestController_StartRemoteMiner_EmptyType(t *testing.T) { assert.Contains(t, err.Error(), "miner type is required") } -func TestController_StartRemoteMiner_NoIdentity(t *testing.T) { +func TestController_StartRemoteMiner_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) // Create a node without identity - nmNoID, err := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), - ) + keyPath, configPath := testNodeManagerPaths(t.TempDir()) + nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -565,7 +561,7 @@ func TestController_StartRemoteMiner_NoIdentity(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_StopRemoteMiner(t *testing.T) { +func TestController_StopRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -573,7 +569,7 @@ func TestController_StopRemoteMiner(t *testing.T) { require.NoError(t, err, "StopRemoteMiner should succeed for existing miner") } -func TestController_StopRemoteMiner_NotFound(t *testing.T) { +func TestController_StopRemoteMiner_NotFound_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -581,12 +577,10 @@ func TestController_StopRemoteMiner_NotFound(t *testing.T) { require.Error(t, err, "StopRemoteMiner should fail for non-existent miner") } -func TestController_StopRemoteMiner_NoIdentity(t *testing.T) { +func TestController_StopRemoteMiner_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) - nmNoID, err := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), - ) + keyPath, configPath := testNodeManagerPaths(t.TempDir()) + nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -596,7 +590,7 @@ func TestController_StopRemoteMiner_NoIdentity(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_GetRemoteLogs(t *testing.T) { +func TestController_GetRemoteLogs_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -607,7 +601,7 @@ func TestController_GetRemoteLogs(t *testing.T) { assert.Contains(t, lines[0], "started") } -func TestController_GetRemoteLogs_LimitedLines(t *testing.T) { +func TestController_GetRemoteLogs_LimitedLines_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -616,12 +610,10 @@ func TestController_GetRemoteLogs_LimitedLines(t *testing.T) { assert.Len(t, lines, 1, "should return only 1 line") } -func TestController_GetRemoteLogs_NoIdentity(t *testing.T) { +func TestController_GetRemoteLogs_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) - nmNoID, err := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), - ) + keyPath, configPath := testNodeManagerPaths(t.TempDir()) + nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -631,7 +623,7 @@ func TestController_GetRemoteLogs_NoIdentity(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_GetRemoteStats_WithMiners(t *testing.T) { +func TestController_GetRemoteStats_WithMiners_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.GetIdentity().ID @@ -645,12 +637,10 @@ func TestController_GetRemoteStats_WithMiners(t *testing.T) { assert.Equal(t, 1234.5, stats.Miners[0].Hashrate) } -func TestController_GetRemoteStats_NoIdentity(t *testing.T) { +func TestController_GetRemoteStats_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) - nmNoID, err := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), - ) + keyPath, configPath := testNodeManagerPaths(t.TempDir()) + nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -660,7 +650,7 @@ func TestController_GetRemoteStats_NoIdentity(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_ConnectToPeer_Success(t *testing.T) { +func TestController_ConnectToPeer_Success_Good(t *testing.T) { tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) @@ -684,7 +674,7 @@ func TestController_ConnectToPeer_Success(t *testing.T) { assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection after ConnectToPeer") } -func TestController_HandleResponse_NonReply(t *testing.T) { +func TestController_HandleResponse_NonReply_Good(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -699,7 +689,7 @@ func TestController_HandleResponse_NonReply(t *testing.T) { assert.Equal(t, 0, count) } -func TestController_HandleResponse_FullChannel(t *testing.T) { +func TestController_HandleResponse_FullChannel_Ugly(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -723,12 +713,10 @@ func TestController_HandleResponse_FullChannel(t *testing.T) { assert.False(t, exists, "pending entry should be removed after handling") } -func TestController_PingPeer_NoIdentity(t *testing.T) { +func TestController_PingPeer_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) - nmNoID, _ := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), - ) + keyPath, configPath := testNodeManagerPaths(t.TempDir()) + nmNoID, _ := NewNodeManagerWithPaths(keyPath, configPath) controller := NewController(nmNoID, tp.ClientReg, tp.Client) _, err := controller.PingPeer("some-peer") diff --git a/node/dispatcher.go b/node/dispatcher.go index 32899c9..276be96 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -28,19 +28,27 @@ const ( // IntentHandler processes a UEPS packet that has been routed by intent. // Implementations receive the fully parsed and HMAC-verified packet. +// +// var handler IntentHandler = func(pkt *ueps.ParsedPacket) error { return nil } type IntentHandler func(pkt *ueps.ParsedPacket) error // Dispatcher routes verified UEPS packets to registered intent handlers. // It enforces a threat circuit breaker before routing: any packet whose // ThreatScore exceeds ThreatScoreThreshold is dropped and logged. // +// dispatcher := NewDispatcher() +// // Design decisions: +// // - Handlers are registered per IntentID (1:1 mapping). +// // - Unknown intents are logged at WARN level and silently dropped (no error // returned to the caller) to avoid back-pressure on the transport layer. +// // - High-threat packets are dropped silently (logged at WARN) rather than // returning an error, consistent with the "don't even parse the payload" // philosophy from the original stub. +// // - The dispatcher is safe for concurrent use; a RWMutex protects the // handler map. type Dispatcher struct { @@ -50,6 +58,8 @@ type Dispatcher struct { } // NewDispatcher creates a Dispatcher with no registered handlers. +// +// dispatcher := NewDispatcher() func NewDispatcher() *Dispatcher { return &Dispatcher{ handlers: make(map[byte]IntentHandler), diff --git a/node/dispatcher_test.go b/node/dispatcher_test.go index f817c03..c65d1b1 100644 --- a/node/dispatcher_test.go +++ b/node/dispatcher_test.go @@ -1,11 +1,11 @@ package node import ( - "fmt" "sync" "sync/atomic" "testing" + core "dappco.re/go/core" "dappco.re/go/core/p2p/ueps" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,7 +28,7 @@ func makePacket(intentID byte, threatScore uint16, payload []byte) *ueps.ParsedP // --- Dispatcher Tests --- -func TestDispatcher_RegisterAndDispatch(t *testing.T) { +func TestDispatcher_RegisterAndDispatch_Good(t *testing.T) { t.Run("handler receives the correct packet", func(t *testing.T) { d := NewDispatcher() var received *ueps.ParsedPacket @@ -49,7 +49,7 @@ func TestDispatcher_RegisterAndDispatch(t *testing.T) { t.Run("handler error propagates to caller", func(t *testing.T) { d := NewDispatcher() - handlerErr := fmt.Errorf("compute failed") + handlerErr := core.NewError("compute failed") d.RegisterHandler(IntentCompute, func(pkt *ueps.ParsedPacket) error { return handlerErr @@ -62,7 +62,7 @@ func TestDispatcher_RegisterAndDispatch(t *testing.T) { }) } -func TestDispatcher_ThreatCircuitBreaker(t *testing.T) { +func TestDispatcher_ThreatCircuitBreaker_Good(t *testing.T) { tests := []struct { name string threatScore uint16 @@ -118,7 +118,7 @@ func TestDispatcher_ThreatCircuitBreaker(t *testing.T) { } } -func TestDispatcher_UnknownIntentDropped(t *testing.T) { +func TestDispatcher_UnknownIntentDropped_Bad(t *testing.T) { d := NewDispatcher() // Register handlers for known intents only @@ -133,7 +133,7 @@ func TestDispatcher_UnknownIntentDropped(t *testing.T) { assert.ErrorIs(t, err, ErrUnknownIntent) } -func TestDispatcher_MultipleHandlersCorrectRouting(t *testing.T) { +func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { d := NewDispatcher() var handshakeCalled, computeCalled, rehabCalled, customCalled bool @@ -192,7 +192,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting(t *testing.T) { } } -func TestDispatcher_NilAndEmptyPayload(t *testing.T) { +func TestDispatcher_NilAndEmptyPayload_Ugly(t *testing.T) { t.Run("nil packet returns ErrNilPacket", func(t *testing.T) { d := NewDispatcher() err := d.Dispatch(nil) @@ -234,7 +234,7 @@ func TestDispatcher_NilAndEmptyPayload(t *testing.T) { }) } -func TestDispatcher_ConcurrentDispatchSafety(t *testing.T) { +func TestDispatcher_ConcurrentDispatchSafety_Ugly(t *testing.T) { d := NewDispatcher() var count atomic.Int64 @@ -261,7 +261,7 @@ func TestDispatcher_ConcurrentDispatchSafety(t *testing.T) { assert.Equal(t, int64(goroutines), count.Load()) } -func TestDispatcher_ConcurrentRegisterAndDispatch(t *testing.T) { +func TestDispatcher_ConcurrentRegisterAndDispatch_Ugly(t *testing.T) { d := NewDispatcher() var count atomic.Int64 @@ -301,7 +301,7 @@ func TestDispatcher_ConcurrentRegisterAndDispatch(t *testing.T) { assert.True(t, count.Load() >= 0) } -func TestDispatcher_ReplaceHandler(t *testing.T) { +func TestDispatcher_ReplaceHandler_Good(t *testing.T) { d := NewDispatcher() var firstCalled, secondCalled bool @@ -325,7 +325,7 @@ func TestDispatcher_ReplaceHandler(t *testing.T) { assert.True(t, secondCalled, "replacement handler should be called") } -func TestDispatcher_ThreatBlocksBeforeRouting(t *testing.T) { +func TestDispatcher_ThreatBlocksBeforeRouting_Good(t *testing.T) { // Verify that the circuit breaker fires before intent routing, // so even an unknown intent returns ErrThreatScoreExceeded (not ErrUnknownIntent). d := NewDispatcher() @@ -337,7 +337,7 @@ func TestDispatcher_ThreatBlocksBeforeRouting(t *testing.T) { "threat circuit breaker should fire before intent routing") } -func TestDispatcher_IntentConstants(t *testing.T) { +func TestDispatcher_IntentConstants_Good(t *testing.T) { // Verify the well-known intent IDs match the spec (RFC-021). assert.Equal(t, byte(0x01), IntentHandshake) assert.Equal(t, byte(0x20), IntentCompute) diff --git a/node/identity.go b/node/identity.go index 4e4f255..5ec6a88 100644 --- a/node/identity.go +++ b/node/identity.go @@ -20,6 +20,8 @@ import ( const ChallengeSize = 32 // GenerateChallenge creates a random challenge for authentication. +// +// challenge, err := GenerateChallenge() func GenerateChallenge() ([]byte, error) { challenge := make([]byte, ChallengeSize) if _, err := rand.Read(challenge); err != nil { @@ -30,6 +32,8 @@ func GenerateChallenge() ([]byte, error) { // SignChallenge creates an HMAC signature of a challenge using a shared secret. // The signature proves possession of the shared secret without revealing it. +// +// signature := SignChallenge(challenge, sharedSecret) func SignChallenge(challenge []byte, sharedSecret []byte) []byte { mac := hmac.New(sha256.New, sharedSecret) mac.Write(challenge) @@ -37,12 +41,16 @@ func SignChallenge(challenge []byte, sharedSecret []byte) []byte { } // VerifyChallenge verifies that a challenge response was signed with the correct shared secret. +// +// ok := VerifyChallenge(challenge, signature, sharedSecret) func VerifyChallenge(challenge, response, sharedSecret []byte) bool { expected := SignChallenge(challenge, sharedSecret) return hmac.Equal(response, expected) } // NodeRole defines the operational mode of a node. +// +// role := RoleWorker type NodeRole string const ( @@ -55,6 +63,8 @@ const ( ) // NodeIdentity represents the public identity of a node. +// +// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} type NodeIdentity struct { ID string `json:"id"` // Derived from public key (first 16 bytes hex) Name string `json:"name"` // Human-friendly name @@ -64,6 +74,8 @@ type NodeIdentity struct { } // NodeManager handles node identity operations including key generation and storage. +// +// nodeManager, err := NewNodeManager() type NodeManager struct { identity *NodeIdentity privateKey []byte // Never serialized to JSON @@ -74,6 +86,8 @@ type NodeManager struct { } // NewNodeManager creates a new NodeManager, loading existing identity if available. +// +// nodeManager, err := NewNodeManager() func NewNodeManager() (*NodeManager, error) { keyPath, err := xdg.DataFile("lethean-desktop/node/private.key") if err != nil { @@ -90,6 +104,8 @@ func NewNodeManager() (*NodeManager, error) { // NewNodeManagerWithPaths creates a NodeManager with custom paths. // This is primarily useful for testing to avoid xdg path caching issues. +// +// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, diff --git a/node/identity_test.go b/node/identity_test.go index e2af1fb..87c03b4 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -1,35 +1,21 @@ package node import ( - "os" - "path/filepath" "testing" ) // setupTestNodeManager creates a NodeManager with paths in a temp directory. func setupTestNodeManager(t *testing.T) (*NodeManager, func()) { - tmpDir, err := os.MkdirTemp("", "node-identity-test") + tmpDir := t.TempDir() + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir)) if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - keyPath := filepath.Join(tmpDir, "private.key") - configPath := filepath.Join(tmpDir, "node.json") - - nm, err := NewNodeManagerWithPaths(keyPath, configPath) - if err != nil { - os.RemoveAll(tmpDir) t.Fatalf("failed to create node manager: %v", err) } - cleanup := func() { - os.RemoveAll(tmpDir) - } - - return nm, cleanup + return nm, func() {} } -func TestNodeIdentity(t *testing.T) { +func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Run("NewNodeManager", func(t *testing.T) { nm, cleanup := setupTestNodeManager(t) defer cleanup() @@ -75,14 +61,8 @@ func TestNodeIdentity(t *testing.T) { }) t.Run("LoadExistingIdentity", func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "node-load-test") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - keyPath := filepath.Join(tmpDir, "private.key") - configPath := filepath.Join(tmpDir, "node.json") + tmpDir := t.TempDir() + keyPath, configPath := testNodeManagerPaths(tmpDir) // First, create an identity nm1, err := NewNodeManagerWithPaths(keyPath, configPath) @@ -120,16 +100,11 @@ func TestNodeIdentity(t *testing.T) { t.Run("DeriveSharedSecret", func(t *testing.T) { // Create two node managers with separate temp directories - tmpDir1, _ := os.MkdirTemp("", "node1") - tmpDir2, _ := os.MkdirTemp("", "node2") - defer os.RemoveAll(tmpDir1) - defer os.RemoveAll(tmpDir2) + tmpDir1 := t.TempDir() + tmpDir2 := t.TempDir() // Node 1 - nm1, err := NewNodeManagerWithPaths( - filepath.Join(tmpDir1, "private.key"), - filepath.Join(tmpDir1, "node.json"), - ) + nm1, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir1)) if err != nil { t.Fatalf("failed to create node manager 1: %v", err) } @@ -139,10 +114,7 @@ func TestNodeIdentity(t *testing.T) { } // Node 2 - nm2, err := NewNodeManagerWithPaths( - filepath.Join(tmpDir2, "private.key"), - filepath.Join(tmpDir2, "node.json"), - ) + nm2, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir2)) if err != nil { t.Fatalf("failed to create node manager 2: %v", err) } @@ -198,7 +170,7 @@ func TestNodeIdentity(t *testing.T) { }) } -func TestNodeRoles(t *testing.T) { +func TestIdentity_NodeRoles_Good(t *testing.T) { tests := []struct { role NodeRole expected string @@ -217,7 +189,7 @@ func TestNodeRoles(t *testing.T) { } } -func TestChallengeResponse(t *testing.T) { +func TestIdentity_ChallengeResponse_Good(t *testing.T) { t.Run("GenerateChallenge", func(t *testing.T) { challenge, err := GenerateChallenge() if err != nil { @@ -315,21 +287,13 @@ func TestChallengeResponse(t *testing.T) { t.Run("IntegrationWithSharedSecret", func(t *testing.T) { // Create two nodes and test end-to-end challenge-response - tmpDir1, _ := os.MkdirTemp("", "node-challenge-1") - tmpDir2, _ := os.MkdirTemp("", "node-challenge-2") - defer os.RemoveAll(tmpDir1) - defer os.RemoveAll(tmpDir2) + tmpDir1 := t.TempDir() + tmpDir2 := t.TempDir() - nm1, _ := NewNodeManagerWithPaths( - filepath.Join(tmpDir1, "private.key"), - filepath.Join(tmpDir1, "node.json"), - ) + nm1, _ := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir1)) nm1.GenerateIdentity("challenger", RoleDual) - nm2, _ := NewNodeManagerWithPaths( - filepath.Join(tmpDir2, "private.key"), - filepath.Join(tmpDir2, "node.json"), - ) + nm2, _ := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir2)) nm2.GenerateIdentity("responder", RoleDual) // Challenger generates challenge @@ -352,7 +316,7 @@ func TestChallengeResponse(t *testing.T) { }) } -func TestNodeManager_DeriveSharedSecret_NoIdentity(t *testing.T) { +func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { nm, cleanup := setupTestNodeManager(t) defer cleanup() @@ -363,7 +327,7 @@ func TestNodeManager_DeriveSharedSecret_NoIdentity(t *testing.T) { } } -func TestNodeManager_GetIdentity_NilWhenNoIdentity(t *testing.T) { +func TestIdentity_NodeManager_GetIdentity_NilWhenNoIdentity_Bad(t *testing.T) { nm, cleanup := setupTestNodeManager(t) defer cleanup() @@ -373,11 +337,11 @@ func TestNodeManager_GetIdentity_NilWhenNoIdentity(t *testing.T) { } } -func TestNodeManager_Delete_NoFiles(t *testing.T) { +func TestIdentity_NodeManager_Delete_NoFiles_Bad(t *testing.T) { tmpDir := t.TempDir() nm, err := NewNodeManagerWithPaths( - filepath.Join(tmpDir, "nonexistent.key"), - filepath.Join(tmpDir, "nonexistent.json"), + testJoinPath(tmpDir, "nonexistent.key"), + testJoinPath(tmpDir, "nonexistent.json"), ) if err != nil { t.Fatalf("failed to create node manager: %v", err) diff --git a/node/integration_test.go b/node/integration_test.go index 990419f..c31da5a 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -3,11 +3,9 @@ package node import ( "bufio" "bytes" - "encoding/json" "net/http" "net/http/httptest" "net/url" - "path/filepath" "sync" "sync/atomic" "testing" @@ -29,7 +27,7 @@ import ( // 5. Graceful shutdown with disconnect messages // ============================================================================ -func TestIntegration_FullNodeLifecycle(t *testing.T) { +func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // ---------------------------------------------------------------- // Step 1: Identity creation // ---------------------------------------------------------------- @@ -240,7 +238,7 @@ func TestIntegration_FullNodeLifecycle(t *testing.T) { // TestIntegration_SharedSecretAgreement verifies that two independently created // nodes derive the same shared secret via ECDH. -func TestIntegration_SharedSecretAgreement(t *testing.T) { +func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { nodeA := testNode(t, "secret-node-a", RoleDual) nodeB := testNode(t, "secret-node-b", RoleDual) @@ -260,7 +258,7 @@ func TestIntegration_SharedSecretAgreement(t *testing.T) { // TestIntegration_TwoNodeBidirectionalMessages verifies that both nodes // can send and receive encrypted messages after the handshake. -func TestIntegration_TwoNodeBidirectionalMessages(t *testing.T) { +func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.GetIdentity().ID @@ -285,7 +283,7 @@ func TestIntegration_TwoNodeBidirectionalMessages(t *testing.T) { // TestIntegration_MultiPeerTopology verifies that a controller can // simultaneously communicate with multiple workers. -func TestIntegration_MultiPeerTopology(t *testing.T) { +func TestIntegration_MultiPeerTopology_Good(t *testing.T) { controllerNM := testNode(t, "multi-controller", RoleController) controllerReg := testRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) @@ -343,10 +341,9 @@ func TestIntegration_MultiPeerTopology(t *testing.T) { // TestIntegration_IdentityPersistenceAndReload verifies that a node identity // can be generated, persisted, and reloaded from disk. -func TestIntegration_IdentityPersistenceAndReload(t *testing.T) { +func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { dir := t.TempDir() - keyPath := filepath.Join(dir, "private.key") - configPath := filepath.Join(dir, "node.json") + keyPath, configPath := testNodeManagerPaths(dir) // Create and persist identity. nm1, err := NewNodeManagerWithPaths(keyPath, configPath) @@ -386,10 +383,7 @@ func TestIntegration_IdentityPersistenceAndReload(t *testing.T) { // stmfGenerateKeyPair is a helper that generates a keypair and returns // the public key as base64 (for use in DeriveSharedSecret tests). func stmfGenerateKeyPair(dir string) (string, error) { - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { return "", err } @@ -399,10 +393,9 @@ func stmfGenerateKeyPair(dir string) (string, error) { return nm.GetIdentity().PublicKey, nil } - // TestIntegration_UEPSFullRoundTrip exercises a complete UEPS packet // lifecycle: build, sign, transmit (simulated), read, verify, dispatch. -func TestIntegration_UEPSFullRoundTrip(t *testing.T) { +func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { nodeA := testNode(t, "ueps-node-a", RoleController) nodeB := testNode(t, "ueps-node-b", RoleWorker) @@ -453,7 +446,7 @@ func TestIntegration_UEPSFullRoundTrip(t *testing.T) { // TestIntegration_UEPSIntegrityFailure verifies that a tampered UEPS packet // is rejected by HMAC verification. -func TestIntegration_UEPSIntegrityFailure(t *testing.T) { +func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { nodeA := testNode(t, "integrity-a", RoleController) nodeB := testNode(t, "integrity-b", RoleWorker) @@ -484,7 +477,7 @@ func TestIntegration_UEPSIntegrityFailure(t *testing.T) { // TestIntegration_AllowlistHandshakeRejection verifies that a peer not in the // allowlist is rejected during the WebSocket handshake. -func TestIntegration_AllowlistHandshakeRejection(t *testing.T) { +func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { workerNM := testNode(t, "allowlist-worker", RoleWorker) workerReg := testRegistry(t) workerReg.SetAuthMode(PeerAuthAllowlist) @@ -521,7 +514,7 @@ func TestIntegration_AllowlistHandshakeRejection(t *testing.T) { // TestIntegration_AllowlistHandshakeAccepted verifies that an allowlisted // peer can connect successfully. -func TestIntegration_AllowlistHandshakeAccepted(t *testing.T) { +func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { workerNM := testNode(t, "allowlist-worker-ok", RoleWorker) workerReg := testRegistry(t) workerReg.SetAuthMode(PeerAuthAllowlist) @@ -563,7 +556,7 @@ func TestIntegration_AllowlistHandshakeAccepted(t *testing.T) { // TestIntegration_DispatcherWithRealUEPSPackets builds real UEPS packets // from wire bytes and routes them through the dispatcher. -func TestIntegration_DispatcherWithRealUEPSPackets(t *testing.T) { +func TestIntegration_DispatcherWithRealUEPSPackets_Good(t *testing.T) { sharedSecret := make([]byte, 32) for i := range sharedSecret { sharedSecret[i] = byte(i ^ 0x42) @@ -614,7 +607,7 @@ func TestIntegration_DispatcherWithRealUEPSPackets(t *testing.T) { // TestIntegration_MessageSerialiseDeserialise verifies that messages survive // the full serialisation/encryption/decryption/deserialisation pipeline // with all fields intact. -func TestIntegration_MessageSerialiseDeserialise(t *testing.T) { +func TestIntegration_MessageSerialiseDeserialise_Good(t *testing.T) { tp := setupTestTransportPair(t) pc := tp.connectClient(t) @@ -653,14 +646,14 @@ func TestIntegration_MessageSerialiseDeserialise(t *testing.T) { assert.Equal(t, original.ReplyTo, decrypted.ReplyTo) var originalStats, decryptedStats StatsPayload - require.NoError(t, json.Unmarshal(original.Payload, &originalStats)) - require.NoError(t, json.Unmarshal(decrypted.Payload, &decryptedStats)) + testJSONUnmarshal(t, original.Payload, &originalStats) + testJSONUnmarshal(t, decrypted.Payload, &decryptedStats) assert.Equal(t, originalStats, decryptedStats) } // TestIntegration_GetRemoteStats_EndToEnd tests the full stats retrieval flow // across a real WebSocket connection. -func TestIntegration_GetRemoteStats_EndToEnd(t *testing.T) { +func TestIntegration_GetRemoteStats_EndToEnd_Good(t *testing.T) { tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) diff --git a/node/levin/connection.go b/node/levin/connection.go index a3e1a11..20d7c7d 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -28,6 +28,8 @@ const ( // Connection wraps a net.Conn and provides framed Levin packet I/O. // All writes are serialised by an internal mutex, making it safe to call // WritePacket and WriteResponse concurrently from multiple goroutines. +// +// connection := NewConnection(conn) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -44,6 +46,8 @@ type Connection struct { } // NewConnection creates a Connection that wraps conn with sensible defaults. +// +// connection := NewConnection(conn) func NewConnection(conn net.Conn) *Connection { return &Connection{ MaxPayloadSize: MaxPayloadSize, diff --git a/node/levin/connection_test.go b/node/levin/connection_test.go index 84e494c..30af742 100644 --- a/node/levin/connection_test.go +++ b/node/levin/connection_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestConnection_RoundTrip(t *testing.T) { +func TestConnection_RoundTrip_Ugly(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -41,7 +41,7 @@ func TestConnection_RoundTrip(t *testing.T) { assert.Equal(t, payload, data) } -func TestConnection_EmptyPayload(t *testing.T) { +func TestConnection_EmptyPayload_Ugly(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -64,7 +64,7 @@ func TestConnection_EmptyPayload(t *testing.T) { assert.Nil(t, data) } -func TestConnection_Response(t *testing.T) { +func TestConnection_Response_Good(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -91,7 +91,7 @@ func TestConnection_Response(t *testing.T) { assert.Equal(t, payload, data) } -func TestConnection_PayloadTooBig(t *testing.T) { +func TestConnection_PayloadTooBig_Bad(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -125,7 +125,7 @@ func TestConnection_PayloadTooBig(t *testing.T) { require.NoError(t, <-errCh) } -func TestConnection_ReadTimeout(t *testing.T) { +func TestConnection_ReadTimeout_Bad(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -143,7 +143,7 @@ func TestConnection_ReadTimeout(t *testing.T) { assert.True(t, netErr.Timeout(), "expected timeout error") } -func TestConnection_RemoteAddr(t *testing.T) { +func TestConnection_RemoteAddr_Good(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() @@ -153,7 +153,7 @@ func TestConnection_RemoteAddr(t *testing.T) { assert.NotEmpty(t, addr) } -func TestConnection_Close(t *testing.T) { +func TestConnection_Close_Ugly(t *testing.T) { a, b := net.Pipe() defer b.Close() diff --git a/node/levin/header.go b/node/levin/header.go index 189782a..bd7602f 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -48,6 +48,8 @@ var ( ) // Header is the 33-byte packed header that prefixes every Levin message. +// +// header := Header{Command: CommandHandshake, ExpectResponse: true} type Header struct { Signature uint64 PayloadSize uint64 @@ -59,6 +61,8 @@ type Header struct { } // EncodeHeader serialises h into a fixed-size 33-byte array (little-endian). +// +// encoded := EncodeHeader(header) func EncodeHeader(h *Header) [HeaderSize]byte { var buf [HeaderSize]byte binary.LittleEndian.PutUint64(buf[0:8], h.Signature) @@ -77,6 +81,8 @@ func EncodeHeader(h *Header) [HeaderSize]byte { // DecodeHeader deserialises a 33-byte array into a Header, validating // the magic signature. +// +// header, err := DecodeHeader(buf) func DecodeHeader(buf [HeaderSize]byte) (Header, error) { var h Header h.Signature = binary.LittleEndian.Uint64(buf[0:8]) diff --git a/node/levin/header_test.go b/node/levin/header_test.go index 4edfdaf..9288977 100644 --- a/node/levin/header_test.go +++ b/node/levin/header_test.go @@ -11,11 +11,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestHeaderSizeIs33(t *testing.T) { +func TestHeader_SizeIs33_Good(t *testing.T) { assert.Equal(t, 33, HeaderSize) } -func TestEncodeHeader_KnownValues(t *testing.T) { +func TestHeader_EncodeHeader_KnownValues_Good(t *testing.T) { h := &Header{ Signature: Signature, PayloadSize: 256, @@ -56,7 +56,7 @@ func TestEncodeHeader_KnownValues(t *testing.T) { assert.Equal(t, uint32(0), pv) } -func TestEncodeHeader_ExpectResponseFalse(t *testing.T) { +func TestHeader_EncodeHeader_ExpectResponseFalse_Good(t *testing.T) { h := &Header{ Signature: Signature, PayloadSize: 42, @@ -68,7 +68,7 @@ func TestEncodeHeader_ExpectResponseFalse(t *testing.T) { assert.Equal(t, byte(0x00), buf[16]) } -func TestEncodeHeader_NegativeReturnCode(t *testing.T) { +func TestHeader_EncodeHeader_NegativeReturnCode_Good(t *testing.T) { h := &Header{ Signature: Signature, PayloadSize: 0, @@ -81,7 +81,7 @@ func TestEncodeHeader_NegativeReturnCode(t *testing.T) { assert.Equal(t, ReturnErrFormat, rc) } -func TestDecodeHeader_RoundTrip(t *testing.T) { +func TestHeader_DecodeHeader_RoundTrip_Ugly(t *testing.T) { original := &Header{ Signature: Signature, PayloadSize: 1024, @@ -105,7 +105,7 @@ func TestDecodeHeader_RoundTrip(t *testing.T) { assert.Equal(t, original.ProtocolVersion, decoded.ProtocolVersion) } -func TestDecodeHeader_AllCommands(t *testing.T) { +func TestHeader_DecodeHeader_AllCommands_Good(t *testing.T) { commands := []uint32{ CommandHandshake, CommandTimedSync, @@ -131,7 +131,7 @@ func TestDecodeHeader_AllCommands(t *testing.T) { } } -func TestDecodeHeader_BadSignature(t *testing.T) { +func TestHeader_DecodeHeader_BadSignature_Bad(t *testing.T) { h := &Header{ Signature: 0xDEADBEEF, PayloadSize: 0, @@ -143,7 +143,7 @@ func TestDecodeHeader_BadSignature(t *testing.T) { assert.ErrorIs(t, err, ErrBadSignature) } -func TestDecodeHeader_PayloadTooBig(t *testing.T) { +func TestHeader_DecodeHeader_PayloadTooBig_Bad(t *testing.T) { h := &Header{ Signature: Signature, PayloadSize: MaxPayloadSize + 1, @@ -155,7 +155,7 @@ func TestDecodeHeader_PayloadTooBig(t *testing.T) { assert.ErrorIs(t, err, ErrPayloadTooBig) } -func TestDecodeHeader_MaxPayloadExact(t *testing.T) { +func TestHeader_DecodeHeader_MaxPayloadExact_Ugly(t *testing.T) { h := &Header{ Signature: Signature, PayloadSize: MaxPayloadSize, diff --git a/node/levin/storage.go b/node/levin/storage.go index f8896f4..85b1428 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -50,10 +50,14 @@ var ( // Section is an ordered map of named values forming a portable storage section. // Field iteration order is always alphabetical by key for deterministic encoding. +// +// section := Section{"id": StringVal([]byte("peer-1"))} type Section map[string]Value // Value holds a typed portable storage value. Use the constructor functions // (Uint64Val, StringVal, ObjectVal, etc.) to create instances. +// +// value := StringVal([]byte("peer-1")) type Value struct { Type uint8 @@ -77,39 +81,63 @@ type Value struct { // --------------------------------------------------------------------------- // Uint64Val creates a Value of TypeUint64. +// +// value := Uint64Val(42) func Uint64Val(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } // Uint32Val creates a Value of TypeUint32. +// +// value := Uint32Val(42) func Uint32Val(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } // Uint16Val creates a Value of TypeUint16. +// +// value := Uint16Val(42) func Uint16Val(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } // Uint8Val creates a Value of TypeUint8. +// +// value := Uint8Val(42) func Uint8Val(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } // Int64Val creates a Value of TypeInt64. +// +// value := Int64Val(42) func Int64Val(v int64) Value { return Value{Type: TypeInt64, intVal: v} } // Int32Val creates a Value of TypeInt32. +// +// value := Int32Val(42) func Int32Val(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } // Int16Val creates a Value of TypeInt16. +// +// value := Int16Val(42) func Int16Val(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } // Int8Val creates a Value of TypeInt8. +// +// value := Int8Val(42) func Int8Val(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } // BoolVal creates a Value of TypeBool. +// +// value := BoolVal(true) func BoolVal(v bool) Value { return Value{Type: TypeBool, boolVal: v} } // DoubleVal creates a Value of TypeDouble. +// +// value := DoubleVal(3.14) func DoubleVal(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } // StringVal creates a Value of TypeString. The slice is not copied. +// +// value := StringVal([]byte("hello")) func StringVal(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } // ObjectVal creates a Value of TypeObject wrapping a nested Section. +// +// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) func ObjectVal(s Section) Value { return Value{Type: TypeObject, objectVal: s} } // --------------------------------------------------------------------------- @@ -117,21 +145,29 @@ func ObjectVal(s Section) Value { return Value{Type: TypeObject, objectVal: s} } // --------------------------------------------------------------------------- // Uint64ArrayVal creates a typed array of uint64 values. +// +// value := Uint64ArrayVal([]uint64{1, 2, 3}) func Uint64ArrayVal(vs []uint64) Value { return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs} } // Uint32ArrayVal creates a typed array of uint32 values. +// +// value := Uint32ArrayVal([]uint32{1, 2, 3}) func Uint32ArrayVal(vs []uint32) Value { return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs} } // StringArrayVal creates a typed array of byte-string values. +// +// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) func StringArrayVal(vs [][]byte) Value { return Value{Type: ArrayFlag | TypeString, stringArray: vs} } // ObjectArrayVal creates a typed array of Section values. +// +// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) func ObjectArrayVal(vs []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: vs} } @@ -279,6 +315,8 @@ func (v Value) AsSectionArray() ([]Section, error) { // EncodeStorage serialises a Section to the portable storage binary format, // including the 9-byte header. Keys are sorted alphabetically to ensure // deterministic output. +// +// data, err := EncodeStorage(section) func EncodeStorage(s Section) ([]byte, error) { buf := make([]byte, 0, 256) @@ -450,6 +488,8 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { // DecodeStorage deserialises portable storage binary data (including the // 9-byte header) into a Section. +// +// section, err := DecodeStorage(data) func DecodeStorage(data []byte) (Section, error) { if len(data) < StorageHeaderSize { return nil, ErrStorageTruncated diff --git a/node/levin/storage_test.go b/node/levin/storage_test.go index ae16c52..f16f49e 100644 --- a/node/levin/storage_test.go +++ b/node/levin/storage_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEncodeStorage_EmptySection(t *testing.T) { +func TestStorage_EncodeStorage_EmptySection_Ugly(t *testing.T) { s := Section{} data, err := EncodeStorage(s) require.NoError(t, err) @@ -35,7 +35,7 @@ func TestEncodeStorage_EmptySection(t *testing.T) { assert.Equal(t, byte(0x00), data[9]) } -func TestStorage_PrimitivesRoundTrip(t *testing.T) { +func TestStorage_PrimitivesRoundTrip_Ugly(t *testing.T) { s := Section{ "u64": Uint64Val(0xDEADBEEFCAFEBABE), "u32": Uint32Val(0xCAFEBABE), @@ -106,7 +106,7 @@ func TestStorage_PrimitivesRoundTrip(t *testing.T) { assert.Equal(t, 3.141592653589793, pi) } -func TestStorage_NestedObject(t *testing.T) { +func TestStorage_NestedObject_Good(t *testing.T) { inner := Section{ "port": Uint16Val(18080), "host": StringVal([]byte("127.0.0.1")), @@ -138,7 +138,7 @@ func TestStorage_NestedObject(t *testing.T) { assert.Equal(t, []byte("127.0.0.1"), host) } -func TestStorage_Uint64Array(t *testing.T) { +func TestStorage_Uint64Array_Good(t *testing.T) { s := Section{ "heights": Uint64ArrayVal([]uint64{10, 20, 30}), } @@ -154,7 +154,7 @@ func TestStorage_Uint64Array(t *testing.T) { assert.Equal(t, []uint64{10, 20, 30}, arr) } -func TestStorage_StringArray(t *testing.T) { +func TestStorage_StringArray_Good(t *testing.T) { s := Section{ "peers": StringArrayVal([][]byte{[]byte("foo"), []byte("bar")}), } @@ -172,7 +172,7 @@ func TestStorage_StringArray(t *testing.T) { assert.Equal(t, []byte("bar"), arr[1]) } -func TestStorage_ObjectArray(t *testing.T) { +func TestStorage_ObjectArray_Good(t *testing.T) { sections := []Section{ {"id": Uint32Val(1), "name": StringVal([]byte("alice"))}, {"id": Uint32Val(2), "name": StringVal([]byte("bob"))}, @@ -208,7 +208,7 @@ func TestStorage_ObjectArray(t *testing.T) { assert.Equal(t, []byte("bob"), name2) } -func TestDecodeStorage_BadSignature(t *testing.T) { +func TestStorage_DecodeStorage_BadSignature_Bad(t *testing.T) { // Corrupt the first 4 bytes. data := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x01, 0x02, 0x01, 0x01, 0x00} _, err := DecodeStorage(data) @@ -216,16 +216,16 @@ func TestDecodeStorage_BadSignature(t *testing.T) { assert.ErrorIs(t, err, ErrStorageBadSignature) } -func TestDecodeStorage_TooShort(t *testing.T) { +func TestStorage_DecodeStorage_TooShort_Bad(t *testing.T) { _, err := DecodeStorage([]byte{0x01, 0x11}) require.Error(t, err) assert.ErrorIs(t, err, ErrStorageTruncated) } -func TestStorage_ByteIdenticalReencode(t *testing.T) { +func TestStorage_ByteIdenticalReencode_Ugly(t *testing.T) { s := Section{ - "alpha": Uint64Val(999), - "bravo": StringVal([]byte("deterministic")), + "alpha": Uint64Val(999), + "bravo": StringVal([]byte("deterministic")), "charlie": BoolVal(false), "delta": ObjectVal(Section{ "x": Int32Val(-42), @@ -246,7 +246,7 @@ func TestStorage_ByteIdenticalReencode(t *testing.T) { assert.Equal(t, data1, data2, "re-encoded bytes must be identical") } -func TestStorage_TypeMismatchErrors(t *testing.T) { +func TestStorage_TypeMismatchErrors_Bad(t *testing.T) { v := Uint64Val(42) _, err := v.AsUint32() @@ -265,7 +265,7 @@ func TestStorage_TypeMismatchErrors(t *testing.T) { assert.ErrorIs(t, err, ErrStorageTypeMismatch) } -func TestStorage_Uint32Array(t *testing.T) { +func TestStorage_Uint32Array_Good(t *testing.T) { s := Section{ "ports": Uint32ArrayVal([]uint32{8080, 8443, 9090}), } @@ -281,7 +281,7 @@ func TestStorage_Uint32Array(t *testing.T) { assert.Equal(t, []uint32{8080, 8443, 9090}, arr) } -func TestDecodeStorage_BadVersion(t *testing.T) { +func TestStorage_DecodeStorage_BadVersion_Bad(t *testing.T) { // Valid signatures but version 2 instead of 1. data := []byte{0x01, 0x11, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x00} _, err := DecodeStorage(data) @@ -289,11 +289,11 @@ func TestDecodeStorage_BadVersion(t *testing.T) { assert.ErrorIs(t, err, ErrStorageBadVersion) } -func TestStorage_EmptyArrays(t *testing.T) { +func TestStorage_EmptyArrays_Ugly(t *testing.T) { s := Section{ - "empty_u64": Uint64ArrayVal([]uint64{}), - "empty_str": StringArrayVal([][]byte{}), - "empty_obj": ObjectArrayVal([]Section{}), + "empty_u64": Uint64ArrayVal([]uint64{}), + "empty_str": StringArrayVal([][]byte{}), + "empty_obj": ObjectArrayVal([]Section{}), } data, err := EncodeStorage(s) @@ -315,7 +315,7 @@ func TestStorage_EmptyArrays(t *testing.T) { assert.Empty(t, objarr) } -func TestStorage_BoolFalseRoundTrip(t *testing.T) { +func TestStorage_BoolFalseRoundTrip_Ugly(t *testing.T) { s := Section{ "off": BoolVal(false), "on": BoolVal(true), diff --git a/node/levin/varint.go b/node/levin/varint.go index edbe7e7..97d3e93 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -31,6 +31,8 @@ var ErrVarintOverflow = core.E("levin", "varint overflow", nil) // PackVarint encodes v using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; // the remaining bits carry the value in little-endian order. +// +// encoded := PackVarint(42) func PackVarint(v uint64) []byte { switch { case v <= varintMax1: @@ -55,6 +57,8 @@ func PackVarint(v uint64) []byte { // UnpackVarint decodes one epee portable-storage varint from buf. // It returns the decoded value, the number of bytes consumed, and any error. +// +// value, err := UnpackVarint(data) func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { if len(buf) == 0 { return 0, 0, ErrVarintTruncated diff --git a/node/levin/varint_test.go b/node/levin/varint_test.go index 2082864..948e4bd 100644 --- a/node/levin/varint_test.go +++ b/node/levin/varint_test.go @@ -10,41 +10,41 @@ import ( "github.com/stretchr/testify/require" ) -func TestPackVarint_Value5(t *testing.T) { +func TestVarint_PackVarint_Value5_Good(t *testing.T) { // 5 << 2 | 0x00 = 20 = 0x14 got := PackVarint(5) assert.Equal(t, []byte{0x14}, got) } -func TestPackVarint_Value100(t *testing.T) { +func TestVarint_PackVarint_Value100_Good(t *testing.T) { // 100 << 2 | 0x01 = 401 = 0x0191 → LE [0x91, 0x01] got := PackVarint(100) assert.Equal(t, []byte{0x91, 0x01}, got) } -func TestPackVarint_Value65536(t *testing.T) { +func TestVarint_PackVarint_Value65536_Good(t *testing.T) { // 65536 << 2 | 0x02 = 262146 = 0x00040002 → LE [0x02, 0x00, 0x04, 0x00] got := PackVarint(65536) assert.Equal(t, []byte{0x02, 0x00, 0x04, 0x00}, got) } -func TestPackVarint_Value2Billion(t *testing.T) { +func TestVarint_PackVarint_Value2Billion_Good(t *testing.T) { got := PackVarint(2_000_000_000) require.Len(t, got, 8) // Low 2 bits must be 0x03 (8-byte mark). assert.Equal(t, byte(0x03), got[0]&0x03) } -func TestPackVarint_Zero(t *testing.T) { +func TestVarint_PackVarint_Zero_Ugly(t *testing.T) { got := PackVarint(0) assert.Equal(t, []byte{0x00}, got) } -func TestPackVarint_Boundaries(t *testing.T) { +func TestVarint_PackVarint_Boundaries_Good(t *testing.T) { tests := []struct { - name string - value uint64 - wantLen int + name string + value uint64 + wantLen int }{ {"1-byte max (63)", 63, 1}, {"2-byte min (64)", 64, 2}, @@ -63,7 +63,7 @@ func TestPackVarint_Boundaries(t *testing.T) { } } -func TestVarint_RoundTrip(t *testing.T) { +func TestVarint_RoundTrip_Ugly(t *testing.T) { values := []uint64{ 0, 1, 63, 64, 100, 16_383, 16_384, 1_073_741_823, 1_073_741_824, @@ -79,13 +79,13 @@ func TestVarint_RoundTrip(t *testing.T) { } } -func TestUnpackVarint_EmptyInput(t *testing.T) { +func TestVarint_UnpackVarint_EmptyInput_Ugly(t *testing.T) { _, _, err := UnpackVarint([]byte{}) require.Error(t, err) assert.ErrorIs(t, err, ErrVarintTruncated) } -func TestUnpackVarint_Truncated2Byte(t *testing.T) { +func TestVarint_UnpackVarint_Truncated2Byte_Bad(t *testing.T) { // Encode 64 (needs 2 bytes), then only pass 1 byte. buf := PackVarint(64) require.Len(t, buf, 2) @@ -94,7 +94,7 @@ func TestUnpackVarint_Truncated2Byte(t *testing.T) { assert.ErrorIs(t, err, ErrVarintTruncated) } -func TestUnpackVarint_Truncated4Byte(t *testing.T) { +func TestVarint_UnpackVarint_Truncated4Byte_Bad(t *testing.T) { buf := PackVarint(16_384) require.Len(t, buf, 4) _, _, err := UnpackVarint(buf[:2]) @@ -102,7 +102,7 @@ func TestUnpackVarint_Truncated4Byte(t *testing.T) { assert.ErrorIs(t, err, ErrVarintTruncated) } -func TestUnpackVarint_Truncated8Byte(t *testing.T) { +func TestVarint_UnpackVarint_Truncated8Byte_Bad(t *testing.T) { buf := PackVarint(1_073_741_824) require.Len(t, buf, 8) _, _, err := UnpackVarint(buf[:4]) @@ -110,7 +110,7 @@ func TestUnpackVarint_Truncated8Byte(t *testing.T) { assert.ErrorIs(t, err, ErrVarintTruncated) } -func TestUnpackVarint_ExtraBytes(t *testing.T) { +func TestVarint_UnpackVarint_ExtraBytes_Good(t *testing.T) { // Ensure that extra trailing bytes are not consumed. buf := append(PackVarint(42), 0xFF, 0xFF) decoded, consumed, err := UnpackVarint(buf) @@ -119,7 +119,7 @@ func TestUnpackVarint_ExtraBytes(t *testing.T) { assert.Equal(t, 1, consumed) } -func TestPackVarint_SizeMarkBits(t *testing.T) { +func TestVarint_PackVarint_SizeMarkBits_Good(t *testing.T) { tests := []struct { name string value uint64 diff --git a/node/message.go b/node/message.go index b90ef0b..a10aa96 100644 --- a/node/message.go +++ b/node/message.go @@ -1,7 +1,6 @@ package node import ( - "encoding/json" "slices" "time" @@ -19,17 +18,49 @@ const ( // SupportedProtocolVersions lists all protocol versions this node supports. // Used for version negotiation during handshake. +// +// versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} -// RawMessage is the message payload byte slice used for deferred JSON decoding. -type RawMessage = json.RawMessage +// RawMessage stores an already-encoded JSON payload for deferred decoding. +// +// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) +type RawMessage []byte + +// MarshalJSON preserves the raw JSON payload when the message is encoded. +// +// data, err := RawMessage(`{"ok":true}`).MarshalJSON() +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + + return m, nil +} + +// UnmarshalJSON stores the raw JSON payload bytes without decoding them. +// +// var payload RawMessage +// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) +func (m *RawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return core.E("node.RawMessage.UnmarshalJSON", "raw message target is nil", nil) + } + + *m = append((*m)[:0], data...) + return nil +} // IsProtocolVersionSupported checks if a given version is supported. +// +// ok := IsProtocolVersionSupported("1.0") func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } // MessageType defines the type of P2P message. +// +// msgType := MsgPing type MessageType string const ( @@ -60,6 +91,8 @@ const ( ) // Message represents a P2P message between nodes. +// +// msg, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -71,6 +104,8 @@ type Message struct { } // NewMessage creates a new message with a generated ID and timestamp. +// +// msg, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: 42}) func NewMessage(msgType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -78,7 +113,7 @@ func NewMessage(msgType MessageType, from, to string, payload any) (*Message, er if err != nil { return nil, err } - payloadBytes = data + payloadBytes = RawMessage(data) } return &Message{ @@ -116,6 +151,8 @@ func (m *Message) ParsePayload(v any) error { // --- Payload Types --- // HandshakePayload is sent during connection establishment. +// +// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} type HandshakePayload struct { Identity NodeIdentity `json:"identity"` Challenge []byte `json:"challenge,omitempty"` // Random bytes for auth @@ -123,6 +160,8 @@ type HandshakePayload struct { } // HandshakeAckPayload is the response to a handshake. +// +// ack := HandshakeAckPayload{Accepted: true} type HandshakeAckPayload struct { Identity NodeIdentity `json:"identity"` ChallengeResponse []byte `json:"challengeResponse,omitempty"` @@ -131,17 +170,23 @@ type HandshakeAckPayload struct { } // PingPayload for keepalive/latency measurement. +// +// payload := PingPayload{SentAt: 42} type PingPayload struct { SentAt int64 `json:"sentAt"` // Unix timestamp in milliseconds } // PongPayload response to ping. +// +// payload := PongPayload{SentAt: 42, ReceivedAt: 43} type PongPayload struct { SentAt int64 `json:"sentAt"` // Echo of ping's sentAt ReceivedAt int64 `json:"receivedAt"` // When ping was received } // StartMinerPayload requests starting a miner. +// +// payload := StartMinerPayload{MinerType: "xmrig"} type StartMinerPayload struct { MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") ProfileID string `json:"profileId,omitempty"` @@ -149,11 +194,15 @@ type StartMinerPayload struct { } // StopMinerPayload requests stopping a miner. +// +// payload := StopMinerPayload{MinerName: "xmrig-0"} type StopMinerPayload struct { MinerName string `json:"minerName"` } // MinerAckPayload acknowledges a miner start/stop operation. +// +// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} type MinerAckPayload struct { Success bool `json:"success"` MinerName string `json:"minerName,omitempty"` @@ -161,6 +210,8 @@ type MinerAckPayload struct { } // MinerStatsItem represents stats for a single miner. +// +// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} type MinerStatsItem struct { Name string `json:"name"` Type string `json:"type"` @@ -174,6 +225,8 @@ type MinerStatsItem struct { } // StatsPayload contains miner statistics. +// +// stats := StatsPayload{NodeID: "worker-1"} type StatsPayload struct { NodeID string `json:"nodeId"` NodeName string `json:"nodeName"` @@ -182,6 +235,8 @@ type StatsPayload struct { } // GetLogsPayload requests console logs from a miner. +// +// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} type GetLogsPayload struct { MinerName string `json:"minerName"` Lines int `json:"lines"` // Number of lines to fetch @@ -189,6 +244,8 @@ type GetLogsPayload struct { } // LogsPayload contains console log lines. +// +// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} type LogsPayload struct { MinerName string `json:"minerName"` Lines []string `json:"lines"` @@ -196,6 +253,8 @@ type LogsPayload struct { } // DeployPayload contains a deployment bundle. +// +// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} type DeployPayload struct { BundleType string `json:"type"` // "profile" | "miner" | "full" Data []byte `json:"data"` // STIM-encrypted bundle @@ -204,6 +263,8 @@ type DeployPayload struct { } // DeployAckPayload acknowledges a deployment. +// +// ack := DeployAckPayload{Success: true, Name: "xmrig"} type DeployAckPayload struct { Success bool `json:"success"` Name string `json:"name,omitempty"` @@ -211,6 +272,8 @@ type DeployAckPayload struct { } // ErrorPayload contains error information. +// +// payload := ErrorPayload{Code: ErrCodeOperationFailed, Message: "start failed"} type ErrorPayload struct { Code int `json:"code"` Message string `json:"message"` @@ -228,6 +291,8 @@ const ( ) // NewErrorMessage creates an error response message. +// +// msg, err := NewErrorMessage("worker", "controller", ErrCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { msg, err := NewMessage(MsgError, from, to, ErrorPayload{ Code: code, diff --git a/node/message_test.go b/node/message_test.go index 4443470..db1d9e3 100644 --- a/node/message_test.go +++ b/node/message_test.go @@ -1,12 +1,11 @@ package node import ( - "encoding/json" "testing" "time" ) -func TestNewMessage(t *testing.T) { +func TestMessage_NewMessage_Good(t *testing.T) { t.Run("BasicMessage", func(t *testing.T) { msg, err := NewMessage(MsgPing, "sender-id", "receiver-id", nil) if err != nil { @@ -60,7 +59,7 @@ func TestNewMessage(t *testing.T) { }) } -func TestMessageReply(t *testing.T) { +func TestMessage_Reply_Good(t *testing.T) { original, _ := NewMessage(MsgPing, "sender", "receiver", PingPayload{SentAt: 12345}) reply, err := original.Reply(MsgPong, PongPayload{ @@ -89,7 +88,7 @@ func TestMessageReply(t *testing.T) { } } -func TestParsePayload(t *testing.T) { +func TestMessage_ParsePayload_Good(t *testing.T) { t.Run("ValidPayload", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", @@ -160,7 +159,7 @@ func TestParsePayload(t *testing.T) { }) } -func TestNewErrorMessage(t *testing.T) { +func TestMessage_NewErrorMessage_Bad(t *testing.T) { errMsg, err := NewErrorMessage("sender", "receiver", ErrCodeOperationFailed, "something went wrong", "original-msg-id") if err != nil { t.Fatalf("failed to create error message: %v", err) @@ -189,24 +188,18 @@ func TestNewErrorMessage(t *testing.T) { } } -func TestMessageSerialization(t *testing.T) { +func TestMessage_Serialization_Good(t *testing.T) { original, _ := NewMessage(MsgStartMiner, "ctrl", "worker", StartMinerPayload{ MinerType: "xmrig", ProfileID: "my-profile", }) // Serialize - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("failed to serialize message: %v", err) - } + data := testJSONMarshal(t, original) // Deserialize var restored Message - err = json.Unmarshal(data, &restored) - if err != nil { - t.Fatalf("failed to deserialize message: %v", err) - } + testJSONUnmarshal(t, data, &restored) if restored.ID != original.ID { t.Error("ID mismatch after serialization") @@ -221,8 +214,7 @@ func TestMessageSerialization(t *testing.T) { } var payload StartMinerPayload - err = restored.ParsePayload(&payload) - if err != nil { + if err := restored.ParsePayload(&payload); err != nil { t.Fatalf("failed to parse restored payload: %v", err) } @@ -231,7 +223,7 @@ func TestMessageSerialization(t *testing.T) { } } -func TestMessageTypes(t *testing.T) { +func TestMessage_Types_Good(t *testing.T) { types := []MessageType{ MsgHandshake, MsgHandshakeAck, @@ -264,7 +256,7 @@ func TestMessageTypes(t *testing.T) { } } -func TestErrorCodes(t *testing.T) { +func TestMessage_ErrorCodes_Bad(t *testing.T) { codes := map[int]string{ ErrCodeUnknown: "Unknown", ErrCodeInvalidMessage: "InvalidMessage", @@ -283,7 +275,7 @@ func TestErrorCodes(t *testing.T) { } } -func TestNewMessage_NilPayload(t *testing.T) { +func TestMessage_NewMessage_NilPayload_Ugly(t *testing.T) { msg, err := NewMessage(MsgPing, "from", "to", nil) if err != nil { t.Fatalf("NewMessage with nil payload should succeed: %v", err) @@ -293,7 +285,7 @@ func TestNewMessage_NilPayload(t *testing.T) { } } -func TestMessage_ParsePayload_Nil(t *testing.T) { +func TestMessage_ParsePayload_Nil_Ugly(t *testing.T) { msg := &Message{Payload: nil} var target PingPayload err := msg.ParsePayload(&target) @@ -302,7 +294,7 @@ func TestMessage_ParsePayload_Nil(t *testing.T) { } } -func TestNewErrorMessage_Success(t *testing.T) { +func TestMessage_NewErrorMessage_Success_Bad(t *testing.T) { msg, err := NewErrorMessage("from", "to", ErrCodeOperationFailed, "something went wrong", "reply-123") if err != nil { t.Fatalf("NewErrorMessage failed: %v", err) @@ -315,7 +307,10 @@ func TestNewErrorMessage_Success(t *testing.T) { } var payload ErrorPayload - msg.ParsePayload(&payload) + err = msg.ParsePayload(&payload) + if err != nil { + t.Fatalf("ParsePayload failed: %v", err) + } if payload.Code != ErrCodeOperationFailed { t.Errorf("expected code %d, got %d", ErrCodeOperationFailed, payload.Code) } diff --git a/node/peer.go b/node/peer.go index cedc741..19ce7a9 100644 --- a/node/peer.go +++ b/node/peer.go @@ -16,6 +16,8 @@ import ( ) // Peer represents a known remote node. +// +// peer := &Peer{ID: "worker-1", Address: "127.0.0.1:9101"} type Peer struct { ID string `json:"id"` Name string `json:"name"` @@ -39,6 +41,8 @@ type Peer struct { const saveDebounceInterval = 5 * time.Second // PeerAuthMode controls how unknown peers are handled +// +// mode := PeerAuthAllowlist type PeerAuthMode int const ( @@ -88,6 +92,8 @@ func validatePeerName(name string) error { } // PeerRegistry manages known peers with KD-tree based selection. +// +// peerRegistry, err := NewPeerRegistry() type PeerRegistry struct { peers map[string]*Peer kdTree *poindexter.KDTree[string] // KD-tree with peer ID as payload @@ -117,6 +123,8 @@ var ( ) // NewPeerRegistry creates a new PeerRegistry, loading existing peers if available. +// +// peerRegistry, err := NewPeerRegistry() func NewPeerRegistry() (*PeerRegistry, error) { peersPath, err := xdg.ConfigFile("lethean-desktop/peers.json") if err != nil { @@ -128,6 +136,8 @@ func NewPeerRegistry() (*PeerRegistry, error) { // NewPeerRegistryWithPath creates a new PeerRegistry with a custom path. // This is primarily useful for testing to avoid xdg path caching issues. +// +// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), diff --git a/node/peer_test.go b/node/peer_test.go index 9653cbe..ed4bd70 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -1,35 +1,24 @@ package node import ( - "os" - "path/filepath" "slices" "testing" "time" ) func setupTestPeerRegistry(t *testing.T) (*PeerRegistry, func()) { - tmpDir, err := os.MkdirTemp("", "peer-registry-test") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - peersPath := filepath.Join(tmpDir, "peers.json") + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") pr, err := NewPeerRegistryWithPath(peersPath) if err != nil { - os.RemoveAll(tmpDir) t.Fatalf("failed to create peer registry: %v", err) } - cleanup := func() { - os.RemoveAll(tmpDir) - } - - return pr, cleanup + return pr, func() {} } -func TestPeerRegistry_NewPeerRegistry(t *testing.T) { +func TestPeer_Registry_NewPeerRegistry_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -38,7 +27,7 @@ func TestPeerRegistry_NewPeerRegistry(t *testing.T) { } } -func TestPeerRegistry_AddPeer(t *testing.T) { +func TestPeer_Registry_AddPeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -67,7 +56,7 @@ func TestPeerRegistry_AddPeer(t *testing.T) { } } -func TestPeerRegistry_GetPeer(t *testing.T) { +func TestPeer_Registry_GetPeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -97,7 +86,7 @@ func TestPeerRegistry_GetPeer(t *testing.T) { } } -func TestPeerRegistry_ListPeers(t *testing.T) { +func TestPeer_Registry_ListPeers_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -117,7 +106,7 @@ func TestPeerRegistry_ListPeers(t *testing.T) { } } -func TestPeerRegistry_RemovePeer(t *testing.T) { +func TestPeer_Registry_RemovePeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -150,7 +139,7 @@ func TestPeerRegistry_RemovePeer(t *testing.T) { } } -func TestPeerRegistry_UpdateMetrics(t *testing.T) { +func TestPeer_Registry_UpdateMetrics_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -183,7 +172,7 @@ func TestPeerRegistry_UpdateMetrics(t *testing.T) { } } -func TestPeerRegistry_UpdateScore(t *testing.T) { +func TestPeer_Registry_UpdateScore_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -237,7 +226,7 @@ func TestPeerRegistry_UpdateScore(t *testing.T) { } } -func TestPeerRegistry_SetConnected(t *testing.T) { +func TestPeer_Registry_SetConnected_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -272,7 +261,7 @@ func TestPeerRegistry_SetConnected(t *testing.T) { } } -func TestPeerRegistry_GetConnectedPeers(t *testing.T) { +func TestPeer_Registry_GetConnectedPeers_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -295,7 +284,7 @@ func TestPeerRegistry_GetConnectedPeers(t *testing.T) { } } -func TestPeerRegistry_SelectOptimalPeer(t *testing.T) { +func TestPeer_Registry_SelectOptimalPeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -321,7 +310,7 @@ func TestPeerRegistry_SelectOptimalPeer(t *testing.T) { } } -func TestPeerRegistry_SelectNearestPeers(t *testing.T) { +func TestPeer_Registry_SelectNearestPeers_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -342,11 +331,9 @@ func TestPeerRegistry_SelectNearestPeers(t *testing.T) { } } -func TestPeerRegistry_Persistence(t *testing.T) { - tmpDir, _ := os.MkdirTemp("", "persist-test") - defer os.RemoveAll(tmpDir) - - peersPath := filepath.Join(tmpDir, "peers.json") +func TestPeer_Registry_Persistence_Good(t *testing.T) { + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") // Create and save pr1, err := NewPeerRegistryWithPath(peersPath) @@ -391,7 +378,7 @@ func TestPeerRegistry_Persistence(t *testing.T) { // --- Security Feature Tests --- -func TestPeerRegistry_AuthMode(t *testing.T) { +func TestPeer_Registry_AuthMode_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -413,7 +400,7 @@ func TestPeerRegistry_AuthMode(t *testing.T) { } } -func TestPeerRegistry_PublicKeyAllowlist(t *testing.T) { +func TestPeer_Registry_PublicKeyAllowlist_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -450,7 +437,7 @@ func TestPeerRegistry_PublicKeyAllowlist(t *testing.T) { } } -func TestPeerRegistry_IsPeerAllowed_OpenMode(t *testing.T) { +func TestPeer_Registry_IsPeerAllowed_OpenMode_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -466,7 +453,7 @@ func TestPeerRegistry_IsPeerAllowed_OpenMode(t *testing.T) { } } -func TestPeerRegistry_IsPeerAllowed_AllowlistMode(t *testing.T) { +func TestPeer_Registry_IsPeerAllowed_AllowlistMode_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -501,7 +488,7 @@ func TestPeerRegistry_IsPeerAllowed_AllowlistMode(t *testing.T) { } } -func TestPeerRegistry_PeerNameValidation(t *testing.T) { +func TestPeer_Registry_PeerNameValidation_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -545,7 +532,7 @@ func TestPeerRegistry_PeerNameValidation(t *testing.T) { } } -func TestPeerRegistry_ScoreRecording(t *testing.T) { +func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -601,7 +588,7 @@ func TestPeerRegistry_ScoreRecording(t *testing.T) { } } -func TestPeerRegistry_GetPeersByScore(t *testing.T) { +func TestPeer_Registry_GetPeersByScore_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -635,7 +622,7 @@ func TestPeerRegistry_GetPeersByScore(t *testing.T) { // --- Additional coverage tests for peer.go --- -func TestSafeKeyPrefix(t *testing.T) { +func TestPeer_SafeKeyPrefix_Good(t *testing.T) { tests := []struct { name string key string @@ -658,7 +645,7 @@ func TestSafeKeyPrefix(t *testing.T) { } } -func TestValidatePeerName(t *testing.T) { +func TestPeer_ValidatePeerName_Good(t *testing.T) { tests := []struct { name string peerName string @@ -691,7 +678,7 @@ func TestValidatePeerName(t *testing.T) { } } -func TestPeerRegistry_AddPeer_EmptyID(t *testing.T) { +func TestPeer_Registry_AddPeer_EmptyID_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -702,7 +689,7 @@ func TestPeerRegistry_AddPeer_EmptyID(t *testing.T) { } } -func TestPeerRegistry_UpdatePeer(t *testing.T) { +func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -735,7 +722,7 @@ func TestPeerRegistry_UpdatePeer(t *testing.T) { } } -func TestPeerRegistry_UpdateMetrics_NotFound(t *testing.T) { +func TestPeer_Registry_UpdateMetrics_NotFound_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -745,7 +732,7 @@ func TestPeerRegistry_UpdateMetrics_NotFound(t *testing.T) { } } -func TestPeerRegistry_UpdateScore_NotFound(t *testing.T) { +func TestPeer_Registry_UpdateScore_NotFound_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -755,7 +742,7 @@ func TestPeerRegistry_UpdateScore_NotFound(t *testing.T) { } } -func TestPeerRegistry_RecordSuccess_NotFound(t *testing.T) { +func TestPeer_Registry_RecordSuccess_NotFound_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -763,21 +750,21 @@ func TestPeerRegistry_RecordSuccess_NotFound(t *testing.T) { pr.RecordSuccess("ghost-peer") } -func TestPeerRegistry_RecordFailure_NotFound(t *testing.T) { +func TestPeer_Registry_RecordFailure_NotFound_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() pr.RecordFailure("ghost-peer") } -func TestPeerRegistry_RecordTimeout_NotFound(t *testing.T) { +func TestPeer_Registry_RecordTimeout_NotFound_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() pr.RecordTimeout("ghost-peer") } -func TestPeerRegistry_SelectOptimalPeer_EmptyRegistry(t *testing.T) { +func TestPeer_Registry_SelectOptimalPeer_EmptyRegistry_Ugly(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -787,7 +774,7 @@ func TestPeerRegistry_SelectOptimalPeer_EmptyRegistry(t *testing.T) { } } -func TestPeerRegistry_SelectNearestPeers_EmptyRegistry(t *testing.T) { +func TestPeer_Registry_SelectNearestPeers_EmptyRegistry_Ugly(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -797,7 +784,7 @@ func TestPeerRegistry_SelectNearestPeers_EmptyRegistry(t *testing.T) { } } -func TestPeerRegistry_SetConnected_NonExistent(t *testing.T) { +func TestPeer_Registry_SetConnected_NonExistent_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -805,7 +792,7 @@ func TestPeerRegistry_SetConnected_NonExistent(t *testing.T) { pr.SetConnected("ghost-peer", true) } -func TestPeerRegistry_Close_NoDirtyData(t *testing.T) { +func TestPeer_Registry_Close_NoDirtyData_Ugly(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -816,11 +803,9 @@ func TestPeerRegistry_Close_NoDirtyData(t *testing.T) { } } -func TestPeerRegistry_Close_WithDirtyData(t *testing.T) { - tmpDir, _ := os.MkdirTemp("", "close-dirty-test") - defer os.RemoveAll(tmpDir) - - peersPath := filepath.Join(tmpDir, "peers.json") +func TestPeer_Registry_Close_WithDirtyData_Ugly(t *testing.T) { + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") pr, err := NewPeerRegistryWithPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) @@ -845,11 +830,9 @@ func TestPeerRegistry_Close_WithDirtyData(t *testing.T) { } } -func TestPeerRegistry_ScheduleSave_Debounce(t *testing.T) { - tmpDir, _ := os.MkdirTemp("", "debounce-test") - defer os.RemoveAll(tmpDir) - - peersPath := filepath.Join(tmpDir, "peers.json") +func TestPeer_Registry_ScheduleSave_Debounce_Ugly(t *testing.T) { + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") pr, err := NewPeerRegistryWithPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) @@ -867,11 +850,9 @@ func TestPeerRegistry_ScheduleSave_Debounce(t *testing.T) { } } -func TestPeerRegistry_SaveNow(t *testing.T) { - tmpDir, _ := os.MkdirTemp("", "savenow-test") - defer os.RemoveAll(tmpDir) - - peersPath := filepath.Join(tmpDir, "subdir", "peers.json") +func TestPeer_Registry_SaveNow_Good(t *testing.T) { + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "subdir", "peers.json") pr, err := NewPeerRegistryWithPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) @@ -888,20 +869,18 @@ func TestPeerRegistry_SaveNow(t *testing.T) { } // Verify the file was written - if _, err := os.Stat(peersPath); os.IsNotExist(err) { + if !fsExists(peersPath) { t.Error("peers.json should exist after saveNow") } } -func TestPeerRegistry_ScheduleSave_TimerFires(t *testing.T) { +func TestPeer_Registry_ScheduleSave_TimerFires_Ugly(t *testing.T) { if testing.Short() { t.Skip("skipping debounce timer test in short mode") } - tmpDir, _ := os.MkdirTemp("", "timer-fire-test") - defer os.RemoveAll(tmpDir) - - peersPath := filepath.Join(tmpDir, "peers.json") + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") pr, err := NewPeerRegistryWithPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) @@ -913,7 +892,7 @@ func TestPeerRegistry_ScheduleSave_TimerFires(t *testing.T) { time.Sleep(6 * time.Second) // The file should have been saved by the timer - if _, err := os.Stat(peersPath); os.IsNotExist(err) { + if !fsExists(peersPath) { t.Error("peers.json should exist after debounce timer fires") } diff --git a/node/protocol.go b/node/protocol.go index 19b55dd..36c51c6 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -5,6 +5,8 @@ import ( ) // ProtocolError represents an error from the remote peer. +// +// err := &ProtocolError{Code: ErrCodeOperationFailed, Message: "start failed"} type ProtocolError struct { Code int Message string @@ -15,6 +17,8 @@ func (e *ProtocolError) Error() string { } // ResponseHandler provides helpers for handling protocol responses. +// +// handler := &ResponseHandler{} type ResponseHandler struct{} // ValidateResponse checks if the response is valid and returns a parsed error if it's an error response. @@ -64,22 +68,30 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. +// +// err := ValidateResponse(msg, MsgStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } // ParseResponse is a convenience function using the default handler. +// +// err := ParseResponse(msg, MsgStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } // IsProtocolError returns true if the error is a ProtocolError. +// +// ok := IsProtocolError(err) func IsProtocolError(err error) bool { _, ok := err.(*ProtocolError) return ok } // GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. +// +// code := GetProtocolErrorCode(err) func GetProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code diff --git a/node/protocol_test.go b/node/protocol_test.go index 1d728a4..92535c6 100644 --- a/node/protocol_test.go +++ b/node/protocol_test.go @@ -1,11 +1,12 @@ package node import ( - "fmt" "testing" + + core "dappco.re/go/core" ) -func TestResponseHandler_ValidateResponse(t *testing.T) { +func TestProtocol_ResponseHandler_ValidateResponse_Good(t *testing.T) { handler := &ResponseHandler{} t.Run("NilResponse", func(t *testing.T) { @@ -51,7 +52,7 @@ func TestResponseHandler_ValidateResponse(t *testing.T) { }) } -func TestResponseHandler_ParseResponse(t *testing.T) { +func TestProtocol_ResponseHandler_ParseResponse_Good(t *testing.T) { handler := &ResponseHandler{} t.Run("ParseStats", func(t *testing.T) { @@ -119,7 +120,7 @@ func TestResponseHandler_ParseResponse(t *testing.T) { }) } -func TestProtocolError(t *testing.T) { +func TestProtocol_Error_Bad(t *testing.T) { err := &ProtocolError{Code: 1001, Message: "test error"} if err.Error() != "remote error (1001): test error" { @@ -135,7 +136,7 @@ func TestProtocolError(t *testing.T) { } } -func TestConvenienceFunctions(t *testing.T) { +func TestProtocol_ConvenienceFunctions_Good(t *testing.T) { msg, _ := NewMessage(MsgStats, "sender", "receiver", StatsPayload{NodeID: "test"}) // Test ValidateResponse @@ -153,8 +154,8 @@ func TestConvenienceFunctions(t *testing.T) { } } -func TestGetProtocolErrorCode_NonProtocolError(t *testing.T) { - err := fmt.Errorf("regular error") +func TestProtocol_GetProtocolErrorCode_NonProtocolError_Bad(t *testing.T) { + err := core.NewError("regular error") if GetProtocolErrorCode(err) != 0 { t.Error("Expected 0 for non-ProtocolError") } diff --git a/node/transport.go b/node/transport.go index 44bcc36..fe6e282 100644 --- a/node/transport.go +++ b/node/transport.go @@ -30,6 +30,8 @@ const debugLogInterval = 100 const DefaultMaxMessageSize int64 = 1 << 20 // 1MB // TransportConfig configures the WebSocket transport. +// +// cfg := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WSPath string // "/ws" - WebSocket endpoint path @@ -42,6 +44,8 @@ type TransportConfig struct { } // DefaultTransportConfig returns sensible defaults. +// +// cfg := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: ":9091", @@ -54,9 +58,13 @@ func DefaultTransportConfig() TransportConfig { } // MessageHandler processes incoming messages. +// +// var handler MessageHandler = func(conn *PeerConnection, msg *Message) {} type MessageHandler func(conn *PeerConnection, msg *Message) // MessageDeduplicator tracks seen message IDs to prevent duplicate processing +// +// deduplicator := NewMessageDeduplicator(5 * time.Minute) type MessageDeduplicator struct { seen map[string]time.Time mu sync.RWMutex @@ -64,6 +72,8 @@ type MessageDeduplicator struct { } // NewMessageDeduplicator creates a deduplicator with specified TTL +// +// deduplicator := NewMessageDeduplicator(5 * time.Minute) func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator { d := &MessageDeduplicator{ seen: make(map[string]time.Time), @@ -100,6 +110,8 @@ func (d *MessageDeduplicator) Cleanup() { } // Transport manages WebSocket connections with SMSG encryption. +// +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) type Transport struct { config TransportConfig server *http.Server @@ -117,6 +129,8 @@ type Transport struct { } // PeerRateLimiter implements a simple token bucket rate limiter per peer +// +// rateLimiter := NewPeerRateLimiter(100, 50) type PeerRateLimiter struct { tokens int maxTokens int @@ -126,6 +140,8 @@ type PeerRateLimiter struct { } // NewPeerRateLimiter creates a rate limiter with specified messages/second +// +// rateLimiter := NewPeerRateLimiter(100, 50) func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter { return &PeerRateLimiter{ tokens: maxTokens, @@ -158,6 +174,8 @@ func (r *PeerRateLimiter) Allow() bool { } // PeerConnection represents an active connection to a peer. +// +// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { Peer *Peer Conn *websocket.Conn @@ -170,6 +188,8 @@ type PeerConnection struct { } // NewTransport creates a new WebSocket transport. +// +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { ctx, cancel := context.WithCancel(context.Background()) @@ -856,6 +876,8 @@ func (pc *PeerConnection) Close() error { } // DisconnectPayload contains reason for disconnect. +// +// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} type DisconnectPayload struct { Reason string `json:"reason"` Code int `json:"code"` // Optional disconnect code diff --git a/node/transport_test.go b/node/transport_test.go index ffa6e5a..21368b7 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -1,17 +1,15 @@ package node import ( - "encoding/json" "net/http" "net/http/httptest" "net/url" - "path/filepath" - "strings" "sync" "sync/atomic" "testing" "time" + core "dappco.re/go/core" "github.com/gorilla/websocket" ) @@ -21,10 +19,7 @@ import ( func testNode(t *testing.T, name string, role NodeRole) *NodeManager { t.Helper() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("create node manager %q: %v", name, err) } @@ -38,7 +33,7 @@ func testNode(t *testing.T, name string, role NodeRole) *NodeManager { func testRegistry(t *testing.T) *PeerRegistry { t.Helper() dir := t.TempDir() - reg, err := NewPeerRegistryWithPath(filepath.Join(dir, "peers.json")) + reg, err := NewPeerRegistryWithPath(testJoinPath(dir, "peers.json")) if err != nil { t.Fatalf("create registry: %v", err) } @@ -124,7 +119,7 @@ func (tp *testTransportPair) connectClient(t *testing.T) *PeerConnection { // --- Unit Tests for Sub-Components --- -func TestMessageDeduplicator(t *testing.T) { +func TestTransport_MessageDeduplicator_Good(t *testing.T) { t.Run("MarkAndCheck", func(t *testing.T) { d := NewMessageDeduplicator(5 * time.Minute) @@ -175,7 +170,7 @@ func TestMessageDeduplicator(t *testing.T) { }) } -func TestPeerRateLimiter(t *testing.T) { +func TestTransport_PeerRateLimiter_Good(t *testing.T) { t.Run("AllowUpToBurst", func(t *testing.T) { rl := NewPeerRateLimiter(10, 5) @@ -213,7 +208,7 @@ func TestPeerRateLimiter(t *testing.T) { // --- Transport Integration Tests --- -func TestTransport_FullHandshake(t *testing.T) { +func TestTransport_FullHandshake_Good(t *testing.T) { tp := setupTestTransportPair(t) pc := tp.connectClient(t) @@ -243,7 +238,7 @@ func TestTransport_FullHandshake(t *testing.T) { } } -func TestTransport_HandshakeRejectWrongVersion(t *testing.T) { +func TestTransport_HandshakeRejectWrongVersion_Bad(t *testing.T) { tp := setupTestTransportPair(t) // Dial raw WebSocket and send handshake with unsupported version @@ -272,9 +267,7 @@ func TestTransport_HandshakeRejectWrongVersion(t *testing.T) { } var resp Message - if err := json.Unmarshal(respData, &resp); err != nil { - t.Fatalf("unmarshal response: %v", err) - } + testJSONUnmarshal(t, respData, &resp) var ack HandshakeAckPayload resp.ParsePayload(&ack) @@ -282,12 +275,12 @@ func TestTransport_HandshakeRejectWrongVersion(t *testing.T) { if ack.Accepted { t.Error("should reject incompatible protocol version") } - if !strings.Contains(ack.Reason, "incompatible protocol version") { + if !core.Contains(ack.Reason, "incompatible protocol version") { t.Errorf("expected version rejection reason, got: %s", ack.Reason) } } -func TestTransport_HandshakeRejectAllowlist(t *testing.T) { +func TestTransport_HandshakeRejectAllowlist_Bad(t *testing.T) { tp := setupTestTransportPair(t) // Switch server to allowlist mode WITHOUT adding client's key @@ -305,12 +298,12 @@ func TestTransport_HandshakeRejectAllowlist(t *testing.T) { if err == nil { t.Fatal("should reject peer not in allowlist") } - if !strings.Contains(err.Error(), "rejected") { + if !core.Contains(err.Error(), "rejected") { t.Errorf("expected rejection error, got: %v", err) } } -func TestTransport_EncryptedMessageRoundTrip(t *testing.T) { +func TestTransport_EncryptedMessageRoundTrip_Ugly(t *testing.T) { tp := setupTestTransportPair(t) received := make(chan *Message, 1) @@ -353,7 +346,7 @@ func TestTransport_EncryptedMessageRoundTrip(t *testing.T) { } } -func TestTransport_MessageDedup(t *testing.T) { +func TestTransport_MessageDedup_Good(t *testing.T) { tp := setupTestTransportPair(t) var count atomic.Int32 @@ -383,7 +376,7 @@ func TestTransport_MessageDedup(t *testing.T) { } } -func TestTransport_RateLimiting(t *testing.T) { +func TestTransport_RateLimiting_Good(t *testing.T) { tp := setupTestTransportPair(t) var count atomic.Int32 @@ -415,7 +408,7 @@ func TestTransport_RateLimiting(t *testing.T) { } } -func TestTransport_MaxConnsEnforcement(t *testing.T) { +func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { // Server with MaxConns=1 serverNM := testNode(t, "maxconns-server", RoleWorker) serverReg := testRegistry(t) @@ -467,7 +460,7 @@ func TestTransport_MaxConnsEnforcement(t *testing.T) { } } -func TestTransport_KeepaliveTimeout(t *testing.T) { +func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { // Use short keepalive settings so the test is fast serverCfg := DefaultTransportConfig() serverCfg.PingInterval = 100 * time.Millisecond @@ -516,7 +509,7 @@ func TestTransport_KeepaliveTimeout(t *testing.T) { } } -func TestTransport_GracefulClose(t *testing.T) { +func TestTransport_GracefulClose_Ugly(t *testing.T) { tp := setupTestTransportPair(t) received := make(chan *Message, 10) @@ -551,7 +544,7 @@ func TestTransport_GracefulClose(t *testing.T) { } } -func TestTransport_ConcurrentSends(t *testing.T) { +func TestTransport_ConcurrentSends_Ugly(t *testing.T) { tp := setupTestTransportPair(t) var count atomic.Int32 @@ -591,7 +584,7 @@ func TestTransport_ConcurrentSends(t *testing.T) { // --- Additional coverage tests --- -func TestTransport_Broadcast(t *testing.T) { +func TestTransport_Broadcast_Good(t *testing.T) { // Set up a controller with two worker peers connected. controllerNM := testNode(t, "broadcast-controller", RoleController) controllerReg := testRegistry(t) @@ -648,7 +641,7 @@ func TestTransport_Broadcast(t *testing.T) { } } -func TestTransport_BroadcastExcludesSender(t *testing.T) { +func TestTransport_BroadcastExcludesSender_Good(t *testing.T) { // Verify that Broadcast excludes the sender. tp := setupTestTransportPair(t) @@ -675,7 +668,7 @@ func TestTransport_BroadcastExcludesSender(t *testing.T) { } } -func TestTransport_NewTransport_DefaultMaxMessageSize(t *testing.T) { +func TestTransport_NewTransport_DefaultMaxMessageSize_Good(t *testing.T) { nm := testNode(t, "defaults", RoleWorker) reg := testRegistry(t) cfg := TransportConfig{ @@ -692,7 +685,7 @@ func TestTransport_NewTransport_DefaultMaxMessageSize(t *testing.T) { // The actual default is applied at usage time (readLoop, handleWSUpgrade) } -func TestTransport_ConnectedPeers(t *testing.T) { +func TestTransport_ConnectedPeers_Good(t *testing.T) { tp := setupTestTransportPair(t) if tp.Server.ConnectedPeers() != 0 { @@ -707,7 +700,7 @@ func TestTransport_ConnectedPeers(t *testing.T) { } } -func TestTransport_StartAndStop(t *testing.T) { +func TestTransport_StartAndStop_Good(t *testing.T) { nm := testNode(t, "start-test", RoleWorker) reg := testRegistry(t) cfg := DefaultTransportConfig() @@ -729,7 +722,7 @@ func TestTransport_StartAndStop(t *testing.T) { } } -func TestTransport_CheckOrigin(t *testing.T) { +func TestTransport_CheckOrigin_Good(t *testing.T) { nm := testNode(t, "origin-test", RoleWorker) reg := testRegistry(t) cfg := DefaultTransportConfig() diff --git a/node/worker.go b/node/worker.go index c5b504c..0f97063 100644 --- a/node/worker.go +++ b/node/worker.go @@ -12,6 +12,8 @@ import ( // MinerManager interface for the mining package integration. // This allows the node package to interact with mining.Manager without import cycles. +// +// var minerManager MinerManager type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error @@ -20,6 +22,8 @@ type MinerManager interface { } // MinerInstance represents a running miner for stats collection. +// +// var miner MinerInstance type MinerInstance interface { GetName() string GetType() string @@ -28,12 +32,16 @@ type MinerInstance interface { } // ProfileManager interface for profile operations. +// +// var profileManager ProfileManager type ProfileManager interface { GetProfile(id string) (any, error) SaveProfile(profile any) error } // Worker handles incoming messages on a worker node. +// +// worker := NewWorker(nodeManager, transport) type Worker struct { node *NodeManager transport *Transport @@ -44,6 +52,8 @@ type Worker struct { } // NewWorker creates a new Worker instance. +// +// worker := NewWorker(nodeManager, transport) func NewWorker(node *NodeManager, transport *Transport) *Worker { return &Worker{ node: node, diff --git a/node/worker_test.go b/node/worker_test.go index ee3ed31..7244abc 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -2,34 +2,26 @@ package node import ( "encoding/base64" - "encoding/json" - "fmt" - "os" - "path/filepath" "testing" "time" + + core "dappco.re/go/core" ) // setupTestEnv sets up a temporary environment for testing and returns cleanup function func setupTestEnv(t *testing.T) func() { tmpDir := t.TempDir() - os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpDir, "config")) - os.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, "data")) - return func() { - os.Unsetenv("XDG_CONFIG_HOME") - os.Unsetenv("XDG_DATA_HOME") - } + t.Setenv("XDG_CONFIG_HOME", testJoinPath(tmpDir, "config")) + t.Setenv("XDG_DATA_HOME", testJoinPath(tmpDir, "data")) + return func() {} } -func TestNewWorker(t *testing.T) { +func TestWorker_NewWorker_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -37,7 +29,7 @@ func TestNewWorker(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -57,15 +49,12 @@ func TestNewWorker(t *testing.T) { } } -func TestWorker_SetMinerManager(t *testing.T) { +func TestWorker_SetMinerManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -73,7 +62,7 @@ func TestWorker_SetMinerManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -90,15 +79,12 @@ func TestWorker_SetMinerManager(t *testing.T) { } } -func TestWorker_SetProfileManager(t *testing.T) { +func TestWorker_SetProfileManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -106,7 +92,7 @@ func TestWorker_SetProfileManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -123,15 +109,12 @@ func TestWorker_SetProfileManager(t *testing.T) { } } -func TestWorker_HandlePing(t *testing.T) { +func TestWorker_HandlePing_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -139,7 +122,7 @@ func TestWorker_HandlePing(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -187,15 +170,12 @@ func TestWorker_HandlePing(t *testing.T) { } } -func TestWorker_HandleGetStats(t *testing.T) { +func TestWorker_HandleGetStats_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -203,7 +183,7 @@ func TestWorker_HandleGetStats(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -250,15 +230,12 @@ func TestWorker_HandleGetStats(t *testing.T) { } } -func TestWorker_HandleStartMiner_NoManager(t *testing.T) { +func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -266,7 +243,7 @@ func TestWorker_HandleStartMiner_NoManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -293,15 +270,12 @@ func TestWorker_HandleStartMiner_NoManager(t *testing.T) { } } -func TestWorker_HandleStopMiner_NoManager(t *testing.T) { +func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -309,7 +283,7 @@ func TestWorker_HandleStopMiner_NoManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -336,15 +310,12 @@ func TestWorker_HandleStopMiner_NoManager(t *testing.T) { } } -func TestWorker_HandleGetLogs_NoManager(t *testing.T) { +func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -352,7 +323,7 @@ func TestWorker_HandleGetLogs_NoManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -379,15 +350,12 @@ func TestWorker_HandleGetLogs_NoManager(t *testing.T) { } } -func TestWorker_HandleDeploy_Profile(t *testing.T) { +func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -395,7 +363,7 @@ func TestWorker_HandleDeploy_Profile(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -426,15 +394,12 @@ func TestWorker_HandleDeploy_Profile(t *testing.T) { } } -func TestWorker_HandleDeploy_UnknownType(t *testing.T) { +func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -442,7 +407,7 @@ func TestWorker_HandleDeploy_UnknownType(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -472,7 +437,7 @@ func TestWorker_HandleDeploy_UnknownType(t *testing.T) { } } -func TestConvertMinerStats(t *testing.T) { +func TestWorker_ConvertMinerStats_Good(t *testing.T) { tests := []struct { name string rawStats any @@ -573,15 +538,15 @@ type mockMinerManagerFailing struct { } func (m *mockMinerManagerFailing) StartMiner(minerType string, config any) (MinerInstance, error) { - return nil, fmt.Errorf("mining hardware not available") + return nil, core.E("mockMinerManagerFailing.StartMiner", "mining hardware not available", nil) } func (m *mockMinerManagerFailing) StopMiner(name string) error { - return fmt.Errorf("miner %s not found", name) + return core.E("mockMinerManagerFailing.StopMiner", "miner "+name+" not found", nil) } func (m *mockMinerManagerFailing) GetMiner(name string) (MinerInstance, error) { - return nil, fmt.Errorf("miner %s not found", name) + return nil, core.E("mockMinerManagerFailing.GetMiner", "miner "+name+" not found", nil) } // mockProfileManagerFull implements ProfileManager that returns real data. @@ -592,7 +557,7 @@ type mockProfileManagerFull struct { func (m *mockProfileManagerFull) GetProfile(id string) (any, error) { p, ok := m.profiles[id] if !ok { - return nil, fmt.Errorf("profile %s not found", id) + return nil, core.E("mockProfileManagerFull.GetProfile", "profile "+id+" not found", nil) } return p, nil } @@ -605,22 +570,19 @@ func (m *mockProfileManagerFull) SaveProfile(profile any) error { type mockProfileManagerFailing struct{} func (m *mockProfileManagerFailing) GetProfile(id string) (any, error) { - return nil, fmt.Errorf("profile %s not found", id) + return nil, core.E("mockProfileManagerFailing.GetProfile", "profile "+id+" not found", nil) } func (m *mockProfileManagerFailing) SaveProfile(profile any) error { - return fmt.Errorf("save failed") + return core.E("mockProfileManagerFailing.SaveProfile", "save failed", nil) } -func TestWorker_HandleStartMiner_WithManager(t *testing.T) { +func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -628,7 +590,7 @@ func TestWorker_HandleStartMiner_WithManager(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -649,7 +611,7 @@ func TestWorker_HandleStartMiner_WithManager(t *testing.T) { t.Run("WithConfigOverride", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", - Config: json.RawMessage(`{"pool":"test:3333"}`), + Config: RawMessage(`{"pool":"test:3333"}`), } msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) if err != nil { @@ -680,7 +642,7 @@ func TestWorker_HandleStartMiner_WithManager(t *testing.T) { t.Run("EmptyMinerType", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "", - Config: json.RawMessage(`{}`), + Config: RawMessage(`{}`), } msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) if err != nil { @@ -747,7 +709,7 @@ func TestWorker_HandleStartMiner_WithManager(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", - Config: json.RawMessage(`{}`), + Config: RawMessage(`{}`), } msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) if err != nil { @@ -780,26 +742,23 @@ type mockMinerManagerWithStart struct { func (m *mockMinerManagerWithStart) StartMiner(minerType string, config any) (MinerInstance, error) { m.counter++ - name := fmt.Sprintf("%s-%d", minerType, m.counter) + name := core.Sprintf("%s-%d", minerType, m.counter) return &mockMinerInstance{name: name, minerType: minerType}, nil } -func TestWorker_HandleStopMiner_WithManager(t *testing.T) { +func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -851,22 +810,19 @@ func TestWorker_HandleStopMiner_WithManager(t *testing.T) { }) } -func TestWorker_HandleGetLogs_WithManager(t *testing.T) { +func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -961,22 +917,19 @@ func TestWorker_HandleGetLogs_WithManager(t *testing.T) { }) } -func TestWorker_HandleGetStats_WithMinerManager(t *testing.T) { +func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1025,22 +978,19 @@ func TestWorker_HandleGetStats_WithMinerManager(t *testing.T) { } } -func TestWorker_HandleMessage_UnknownType(t *testing.T) { +func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1055,22 +1005,19 @@ func TestWorker_HandleMessage_UnknownType(t *testing.T) { worker.HandleMessage(nil, msg) } -func TestWorker_HandleDeploy_ProfileWithManager(t *testing.T) { +func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1113,22 +1060,19 @@ func TestWorker_HandleDeploy_ProfileWithManager(t *testing.T) { } } -func TestWorker_HandleDeploy_ProfileSaveFails(t *testing.T) { +func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1162,22 +1106,19 @@ func TestWorker_HandleDeploy_ProfileSaveFails(t *testing.T) { } } -func TestWorker_HandleDeploy_MinerBundle(t *testing.T) { +func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1190,8 +1131,8 @@ func TestWorker_HandleDeploy_MinerBundle(t *testing.T) { identity := nm.GetIdentity() tmpDir := t.TempDir() - minerPath := filepath.Join(tmpDir, "test-miner") - os.WriteFile(minerPath, []byte("fake miner binary"), 0755) + minerPath := testJoinPath(tmpDir, "test-miner") + testWriteFile(t, minerPath, []byte("fake miner binary"), 0o755) profileJSON := []byte(`{"pool":"test:3333"}`) @@ -1229,22 +1170,19 @@ func TestWorker_HandleDeploy_MinerBundle(t *testing.T) { } } -func TestWorker_HandleDeploy_FullBundle(t *testing.T) { +func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1255,8 +1193,8 @@ func TestWorker_HandleDeploy_FullBundle(t *testing.T) { identity := nm.GetIdentity() tmpDir := t.TempDir() - minerPath := filepath.Join(tmpDir, "test-miner") - os.WriteFile(minerPath, []byte("miner binary"), 0755) + minerPath := testJoinPath(tmpDir, "test-miner") + testWriteFile(t, minerPath, []byte("miner binary"), 0o755) sharedSecret := []byte("full-secret-key!") bundlePassword := base64.StdEncoding.EncodeToString(sharedSecret) @@ -1288,22 +1226,19 @@ func TestWorker_HandleDeploy_FullBundle(t *testing.T) { } } -func TestWorker_HandleDeploy_MinerBundle_WithProfileManager(t *testing.T) { +func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1317,8 +1252,8 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager(t *testing.T) { identity := nm.GetIdentity() tmpDir := t.TempDir() - minerPath := filepath.Join(tmpDir, "test-miner") - os.WriteFile(minerPath, []byte("miner binary"), 0755) + minerPath := testJoinPath(tmpDir, "test-miner") + testWriteFile(t, minerPath, []byte("miner binary"), 0o755) profileJSON := []byte(`{"pool":"test:3333"}`) sharedSecret := []byte("profile-secret!!") @@ -1352,17 +1287,14 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager(t *testing.T) { } } -func TestWorker_HandleDeploy_InvalidPayload(t *testing.T) { +func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() dir := t.TempDir() - nm, _ := NewNodeManagerWithPaths( - filepath.Join(dir, "private.key"), - filepath.Join(dir, "node.json"), - ) + nm, _ := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) nm.GenerateIdentity("test", RoleWorker) - pr, _ := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, _ := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() @@ -1377,16 +1309,17 @@ func TestWorker_HandleDeploy_InvalidPayload(t *testing.T) { } } -func TestWorker_HandleGetStats_NoIdentity(t *testing.T) { +func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { cleanup := setupTestEnv(t) defer cleanup() + tmpDir := t.TempDir() nm, _ := NewNodeManagerWithPaths( - filepath.Join(t.TempDir(), "priv.key"), - filepath.Join(t.TempDir(), "node.json"), + testJoinPath(tmpDir, "priv.key"), + testJoinPath(tmpDir, "node.json"), ) // Don't generate identity - pr, _ := NewPeerRegistryWithPath(t.TempDir() + "/peers.json") + pr, _ := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() @@ -1398,7 +1331,7 @@ func TestWorker_HandleGetStats_NoIdentity(t *testing.T) { } } -func TestWorker_HandleMessage_IntegrationViaWebSocket(t *testing.T) { +func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) { // Test HandleMessage through real WebSocket -- exercises error response sending path tp := setupTestTransportPair(t) @@ -1414,14 +1347,14 @@ func TestWorker_HandleMessage_IntegrationViaWebSocket(t *testing.T) { // Send start_miner which will fail because no manager is set. // The worker should send an error response via the connection. - err := controller.StartRemoteMiner(serverID, "xmrig", "", json.RawMessage(`{}`)) + err := controller.StartRemoteMiner(serverID, "xmrig", "", RawMessage(`{}`)) // Should get an error back (either protocol error or operation failed) if err == nil { t.Error("expected error when worker has no miner manager") } } -func TestWorker_HandleMessage_GetStats_IntegrationViaWebSocket(t *testing.T) { +func TestWorker_HandleMessage_GetStats_IntegrationViaWebSocket_Good(t *testing.T) { // HandleMessage dispatch for get_stats through real WebSocket tp := setupTestTransportPair(t) diff --git a/ueps/packet.go b/ueps/packet.go index 7241b2a..f03578b 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -22,6 +22,8 @@ const ( ) // UEPSHeader represents the conscious routing metadata +// +// header := UEPSHeader{IntentID: 0x01} type UEPSHeader struct { Version uint8 // Default 0x09 CurrentLayer uint8 @@ -31,12 +33,16 @@ type UEPSHeader struct { } // PacketBuilder helps construct a signed UEPS frame +// +// builder := NewBuilder(0x01, []byte("hello")) type PacketBuilder struct { Header UEPSHeader Payload []byte } // NewBuilder creates a packet context for a specific intent +// +// builder := NewBuilder(0x01, []byte("hello")) func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { return &PacketBuilder{ Header: UEPSHeader{ diff --git a/ueps/packet_coverage_test.go b/ueps/packet_coverage_test.go index 6e1595c..0748420 100644 --- a/ueps/packet_coverage_test.go +++ b/ueps/packet_coverage_test.go @@ -6,10 +6,10 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/binary" - "errors" "io" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,7 +22,7 @@ type failWriter struct { func (f *failWriter) Write(p []byte) (int, error) { if f.remaining <= 0 { - return 0, errors.New("write failed") + return 0, core.NewError("write failed") } f.remaining-- return len(p), nil @@ -30,7 +30,7 @@ func (f *failWriter) Write(p []byte) (int, error) { // TestWriteTLV_TagWriteFails verifies writeTLV returns an error // when the very first Write (the tag byte) fails. -func TestWriteTLV_TagWriteFails(t *testing.T) { +func TestPacketCoverage_WriteTLV_TagWriteFails_Bad(t *testing.T) { w := &failWriter{remaining: 0} err := writeTLV(w, TagVersion, []byte{0x09}) @@ -40,7 +40,7 @@ func TestWriteTLV_TagWriteFails(t *testing.T) { // TestWriteTLV_LengthWriteFails verifies writeTLV returns an error // when the second Write (the length byte) fails. -func TestWriteTLV_LengthWriteFails(t *testing.T) { +func TestPacketCoverage_WriteTLV_LengthWriteFails_Bad(t *testing.T) { w := &failWriter{remaining: 1} err := writeTLV(w, TagVersion, []byte{0x09}) @@ -50,7 +50,7 @@ func TestWriteTLV_LengthWriteFails(t *testing.T) { // TestWriteTLV_ValueWriteFails verifies writeTLV returns an error // when the third Write (the value bytes) fails. -func TestWriteTLV_ValueWriteFails(t *testing.T) { +func TestPacketCoverage_WriteTLV_ValueWriteFails_Bad(t *testing.T) { w := &failWriter{remaining: 2} err := writeTLV(w, TagVersion, []byte{0x09}) @@ -81,7 +81,7 @@ func (r *errorAfterNReader) Read(p []byte) (int, error) { // TestReadAndVerify_PayloadReadError exercises the error branch at // reader.go:51-53 where io.ReadAll fails after the 0xFF tag byte // has been successfully read. -func TestReadAndVerify_PayloadReadError(t *testing.T) { +func TestPacketCoverage_ReadAndVerify_PayloadReadError_Bad(t *testing.T) { // Build a valid packet so we have genuine TLV headers + HMAC. payload := []byte("coverage test") builder := NewBuilder(0x20, payload) @@ -104,7 +104,7 @@ func TestReadAndVerify_PayloadReadError(t *testing.T) { prefix := frame[:payloadTagIdx+1] r := &errorAfterNReader{ data: prefix, - err: errors.New("connection reset"), + err: core.NewError("connection reset"), } _, err = ReadAndVerify(bufio.NewReader(r), testSecret) @@ -115,7 +115,7 @@ func TestReadAndVerify_PayloadReadError(t *testing.T) { // TestReadAndVerify_PayloadReadError_EOF ensures that a truncated payload // (missing bytes after TagPayload) is handled as an I/O error (UnexpectedEOF) // because ReadAndVerify now uses io.ReadFull with the expected length prefix. -func TestReadAndVerify_PayloadReadError_EOF(t *testing.T) { +func TestPacketCoverage_ReadAndVerify_PayloadReadError_EOF_Bad(t *testing.T) { payload := []byte("eof test") builder := NewBuilder(0x20, payload) frame, err := builder.MarshalAndSign(testSecret) @@ -141,7 +141,7 @@ func TestReadAndVerify_PayloadReadError_EOF(t *testing.T) { // TestWriteTLV_AllWritesSucceed confirms the happy path still works // after exercising all error branches — a simple sanity check using // failWriter with enough remaining writes. -func TestWriteTLV_AllWritesSucceed(t *testing.T) { +func TestPacketCoverage_WriteTLV_AllWritesSucceed_Good(t *testing.T) { var buf bytes.Buffer err := writeTLV(&buf, TagVersion, []byte{0x09}) require.NoError(t, err) @@ -149,10 +149,9 @@ func TestWriteTLV_AllWritesSucceed(t *testing.T) { assert.Equal(t, []byte{TagVersion, 0x00, 0x01, 0x09}, buf.Bytes()) } - // TestWriteTLV_FailWriterTable runs the three failure scenarios in // a table-driven fashion for completeness. -func TestWriteTLV_FailWriterTable(t *testing.T) { +func TestPacketCoverage_WriteTLV_FailWriterTable_Bad(t *testing.T) { tests := []struct { name string remaining int @@ -177,7 +176,7 @@ func TestWriteTLV_FailWriterTable(t *testing.T) { // HMAC computation independently of the builder. This also serves as // a cross-check that our errorAfterNReader is not accidentally // corrupting the prefix bytes. -func TestReadAndVerify_ManualPacket_PayloadReadError(t *testing.T) { +func TestPacketCoverage_ReadAndVerify_ManualPacket_PayloadReadError_Bad(t *testing.T) { payload := []byte("manual test") // Build header TLVs diff --git a/ueps/packet_test.go b/ueps/packet_test.go index cff2f39..89c7729 100644 --- a/ueps/packet_test.go +++ b/ueps/packet_test.go @@ -7,14 +7,15 @@ import ( "crypto/sha256" "encoding/binary" "io" - "strings" "testing" + + core "dappco.re/go/core" ) // testSecret is a deterministic shared secret for reproducible tests. var testSecret = []byte("test-shared-secret-32-bytes!!!!!") -func TestPacketBuilder_RoundTrip(t *testing.T) { +func TestPacket_Builder_RoundTrip_Ugly(t *testing.T) { tests := []struct { name string intentID uint8 @@ -84,7 +85,7 @@ func TestPacketBuilder_RoundTrip(t *testing.T) { } } -func TestHMACVerification_TamperedPayload(t *testing.T) { +func TestPacket_HMACVerification_TamperedPayload_Bad(t *testing.T) { builder := NewBuilder(0x20, []byte("original payload")) frame, err := builder.MarshalAndSign(testSecret) if err != nil { @@ -100,12 +101,12 @@ func TestHMACVerification_TamperedPayload(t *testing.T) { if err == nil { t.Fatal("Expected HMAC mismatch error for tampered payload") } - if !strings.Contains(err.Error(), "integrity violation") { + if !core.Contains(err.Error(), "integrity violation") { t.Errorf("Expected integrity violation error, got: %v", err) } } -func TestHMACVerification_TamperedHeader(t *testing.T) { +func TestPacket_HMACVerification_TamperedHeader_Bad(t *testing.T) { builder := NewBuilder(0x20, []byte("test payload")) frame, err := builder.MarshalAndSign(testSecret) if err != nil { @@ -122,12 +123,12 @@ func TestHMACVerification_TamperedHeader(t *testing.T) { if err == nil { t.Fatal("Expected HMAC mismatch error for tampered header") } - if !strings.Contains(err.Error(), "integrity violation") { + if !core.Contains(err.Error(), "integrity violation") { t.Errorf("Expected integrity violation error, got: %v", err) } } -func TestHMACVerification_WrongSharedSecret(t *testing.T) { +func TestPacket_HMACVerification_WrongSharedSecret_Bad(t *testing.T) { builder := NewBuilder(0x20, []byte("secret data")) frame, err := builder.MarshalAndSign([]byte("key-A-used-for-signing!!!!!!!!!!")) if err != nil { @@ -138,12 +139,12 @@ func TestHMACVerification_WrongSharedSecret(t *testing.T) { if err == nil { t.Fatal("Expected HMAC mismatch error for wrong shared secret") } - if !strings.Contains(err.Error(), "integrity violation") { + if !core.Contains(err.Error(), "integrity violation") { t.Errorf("Expected integrity violation error, got: %v", err) } } -func TestEmptyPayload(t *testing.T) { +func TestPacket_EmptyPayload_Ugly(t *testing.T) { tests := []struct { name string payload []byte @@ -175,7 +176,7 @@ func TestEmptyPayload(t *testing.T) { } } -func TestMaxThreatScoreBoundary(t *testing.T) { +func TestPacket_MaxThreatScoreBoundary_Ugly(t *testing.T) { builder := NewBuilder(0x20, []byte("threat boundary")) builder.Header.ThreatScore = 65535 // uint16 max @@ -194,7 +195,7 @@ func TestMaxThreatScoreBoundary(t *testing.T) { } } -func TestMissingHMACTag(t *testing.T) { +func TestPacket_MissingHMACTag_Bad(t *testing.T) { // Craft a packet manually: header TLVs + payload tag, but no HMAC (0x06) var buf bytes.Buffer @@ -214,24 +215,24 @@ func TestMissingHMACTag(t *testing.T) { if err == nil { t.Fatal("Expected 'missing HMAC' error") } - if !strings.Contains(err.Error(), "missing HMAC") { + if !core.Contains(err.Error(), "missing HMAC") { t.Errorf("Expected 'missing HMAC' error, got: %v", err) } } -func TestWriteTLV_ValueTooLarge(t *testing.T) { +func TestPacket_WriteTLV_ValueTooLarge_Bad(t *testing.T) { var buf bytes.Buffer oversized := make([]byte, 65536) // 1 byte over the 65535 limit err := writeTLV(&buf, TagVersion, oversized) if err == nil { t.Fatal("Expected error for TLV value > 65535 bytes") } - if !strings.Contains(err.Error(), "TLV value too large") { + if !core.Contains(err.Error(), "TLV value too large") { t.Errorf("Expected 'TLV value too large' error, got: %v", err) } } -func TestTruncatedPacket(t *testing.T) { +func TestPacket_TruncatedPacket_Bad(t *testing.T) { builder := NewBuilder(0x20, []byte("full payload")) frame, err := builder.MarshalAndSign(testSecret) if err != nil { @@ -256,7 +257,7 @@ func TestTruncatedPacket(t *testing.T) { { name: "CutMidHMAC", cutAt: 20, // Somewhere inside the header TLVs or HMAC - wantErr: "", // Any io error + wantErr: "", // Any io error }, } @@ -267,14 +268,14 @@ func TestTruncatedPacket(t *testing.T) { if err == nil { t.Fatal("Expected error for truncated packet") } - if tc.wantErr != "" && !strings.Contains(err.Error(), tc.wantErr) { + if tc.wantErr != "" && !core.Contains(err.Error(), tc.wantErr) { t.Errorf("Expected error containing %q, got: %v", tc.wantErr, err) } }) } } -func TestUnknownTLVTag(t *testing.T) { +func TestPacket_UnknownTLVTag_Bad(t *testing.T) { // Build a valid packet, then inject an unknown tag before the HMAC. // The unknown tag must be included in signedData for HMAC to pass. payload := []byte("tagged payload") @@ -324,7 +325,7 @@ func TestUnknownTLVTag(t *testing.T) { } } -func TestNewBuilder_Defaults(t *testing.T) { +func TestPacket_NewBuilder_Defaults_Good(t *testing.T) { builder := NewBuilder(0x20, []byte("data")) if builder.Header.Version != 0x09 { @@ -344,7 +345,7 @@ func TestNewBuilder_Defaults(t *testing.T) { } } -func TestThreatScoreBoundaries(t *testing.T) { +func TestPacket_ThreatScoreBoundaries_Good(t *testing.T) { tests := []struct { name string score uint16 @@ -378,7 +379,7 @@ func TestThreatScoreBoundaries(t *testing.T) { } } -func TestWriteTLV_BoundaryLengths(t *testing.T) { +func TestPacket_WriteTLV_BoundaryLengths_Ugly(t *testing.T) { tests := []struct { name string length int @@ -407,9 +408,8 @@ func TestWriteTLV_BoundaryLengths(t *testing.T) { } } - // TestReadAndVerify_EmptyReader verifies behaviour on completely empty input. -func TestReadAndVerify_EmptyReader(t *testing.T) { +func TestPacket_ReadAndVerify_EmptyReader_Ugly(t *testing.T) { _, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(nil)), testSecret) if err == nil { t.Fatal("Expected error for empty reader") diff --git a/ueps/reader.go b/ueps/reader.go index 8546804..6619003 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -12,6 +12,8 @@ import ( ) // ParsedPacket holds the verified data +// +// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} type ParsedPacket struct { Header UEPSHeader Payload []byte @@ -19,6 +21,8 @@ type ParsedPacket struct { // ReadAndVerify reads a UEPS frame from the stream and validates the HMAC. // It consumes the stream up to the end of the packet. +// +// packet, err := ReadAndVerify(reader, sharedSecret) func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) { // Buffer to reconstruct the data for HMAC verification var signedData bytes.Buffer -- 2.45.3 From 3b2105389aa12f10eafbd4a1fb04321516593fa7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 03:24:39 +0000 Subject: [PATCH 04/60] chore: verification pass -- 2.45.3 From 28336079489a7161db88d76842392d81e546f283 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 05:10:20 +0000 Subject: [PATCH 05/60] docs(repo): refresh AX compliance guidance Co-Authored-By: Virgil --- CLAUDE.md | 4 +- CODEX.md | 11 ++++ README.md | 50 +++++++++++---- SESSION-BRIEF.md | 148 +++++++++++-------------------------------- docs/architecture.md | 18 +++--- docs/development.md | 8 +-- docs/discovery.md | 2 +- docs/history.md | 10 ++- docs/index.md | 2 +- docs/routing.md | 17 ++--- docs/ueps.md | 2 +- 11 files changed, 115 insertions(+), 157 deletions(-) create mode 100644 CODEX.md diff --git a/CLAUDE.md b/CLAUDE.md index 27d11bb..b5fd999 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ logging/ — Structured levelled logger with component scoping (stdlib only) ### Data flow -1. **Identity** (`identity.go`) — Ed25519 keypair via Borg STMF. Shared secrets derived via X25519 ECDH + SHA-256. +1. **Identity** (`identity.go`) — X25519 keypair via Borg STMF. Shared secrets derived via X25519 ECDH + SHA-256. 2. **Transport** (`transport.go`) — WebSocket server/client (gorilla/websocket). Handshake exchanges `NodeIdentity` + HMAC-SHA256 challenge-response. Post-handshake messages are Borg SMSG-encrypted. Includes deduplication (5-min TTL), rate limiting (token bucket: 100 burst/50 per sec), and MaxConns enforcement. 3. **Dispatcher** (`dispatcher.go`) — Routes verified UEPS packets to intent handlers. Threat circuit breaker drops packets with `ThreatScore > 50,000` before routing. 4. **Controller** (`controller.go`) — Issues requests to remote peers using a pending-map pattern (`map[string]chan *Message`). Auto-connects to peers on demand. @@ -75,7 +75,7 @@ type ProfileManager interface { - UK English (colour, organisation, centre, behaviour, recognise) - All parameters and return types explicitly annotated -- Tests use `testify` assert/require; table-driven subtests with `t.Run()` +- Tests use `testify` assert/require; prefer table-driven subtests with `t.Run()` when multiple related cases share one shape - Test name suffixes: `_Good` (happy path), `_Bad` (expected errors), `_Ugly` (panic/edge cases) - Licence: EUPL-1.2 — new files need `// SPDX-License-Identifier: EUPL-1.2` - Security-first: do not weaken HMAC, challenge-response, Zip Slip defence, or rate limiting diff --git a/CODEX.md b/CODEX.md new file mode 100644 index 0000000..3ba5218 --- /dev/null +++ b/CODEX.md @@ -0,0 +1,11 @@ + + +# CODEX.md + +Codex-compatible entrypoint for this repository. + +- Treat `CLAUDE.md` as the authoritative local conventions file for commands, architecture notes, coding standards, and commit format. +- Current module path: `dappco.re/go/core/p2p`. +- Verification baseline: `go build ./...`, `go vet ./...`, and `go test ./...`. +- Use conventional commits with `Co-Authored-By: Virgil `. +- If `.core/reference/docs/RFC.md` is absent in the checkout, report that gap explicitly and use the local docs under `docs/` plus the code as the available reference set. diff --git a/README.md b/README.md index a11c72f..c7da1ca 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,52 @@ -[![Go Reference](https://pkg.go.dev/badge/forge.lthn.ai/core/go-p2p.svg)](https://pkg.go.dev/forge.lthn.ai/core/go-p2p) -[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE.md) +[![Go Reference](https://pkg.go.dev/badge/dappco.re/go/core/p2p.svg)](https://pkg.go.dev/dappco.re/go/core/p2p) +[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](CONTRIBUTING.md#license) [![Go Version](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat&logo=go)](go.mod) # go-p2p -P2P mesh networking layer for the Lethean network. Provides Ed25519 node identity, an encrypted WebSocket transport with HMAC-SHA256 challenge-response handshake, KD-tree peer selection across four dimensions (latency, hops, geography, reliability score), UEPS wire protocol (RFC-021) TLV packet builder and reader, UEPS intent routing with a threat circuit breaker, and TIM deployment bundle encryption with Zip Slip and decompression-bomb defences. +P2P mesh networking layer for the Lethean network. Provides X25519 node identity, an encrypted WebSocket transport with HMAC-SHA256 challenge-response handshake, KD-tree peer selection across four dimensions (latency, hops, geography, reliability score), UEPS wire protocol (RFC-021) TLV packet builder and reader, UEPS intent routing with a threat circuit breaker, and TIM deployment bundle encryption with Zip Slip and decompression-bomb defences. -**Module**: `forge.lthn.ai/core/go-p2p` +**Module**: `dappco.re/go/core/p2p` **Licence**: EUPL-1.2 -**Language**: Go 1.25 +**Language**: Go 1.26 ## Quick Start ```go import ( - "forge.lthn.ai/core/go-p2p/node" - "forge.lthn.ai/core/go-p2p/ueps" + "log" + + "dappco.re/go/core/p2p/node" + "dappco.re/go/core/p2p/ueps" ) -// Start a P2P node -identity, _ := node.LoadOrCreateIdentity() -transport := node.NewTransport(identity, node.TransportConfig{ListenAddr: ":9091"}) -transport.Start(ctx) +nm, err := node.NewNodeManager() +if err != nil { + log.Fatal(err) +} +if !nm.HasIdentity() { + if err := nm.GenerateIdentity("worker-1", node.RoleWorker); err != nil { + log.Fatal(err) + } +} -// Build a UEPS packet -pkt, _ := ueps.NewBuilder(ueps.IntentCompute, payload).MarshalAndSign(sharedSecret) +registry, err := node.NewPeerRegistry() +if err != nil { + log.Fatal(err) +} + +transport := node.NewTransport(nm, registry, node.DefaultTransportConfig()) +if err := transport.Start(); err != nil { + log.Fatal(err) +} + +payload := []byte(`{"job":"hashrate"}`) +sharedSecret := make([]byte, 32) +pkt, err := ueps.NewBuilder(node.IntentCompute, payload).MarshalAndSign(sharedSecret) +if err != nil { + log.Fatal(err) +} +_ = pkt ``` ## Documentation @@ -44,4 +66,4 @@ go build ./... ## Licence -European Union Public Licence 1.2 — see [LICENCE](LICENCE) for details. +European Union Public Licence 1.2 — see [CONTRIBUTING](CONTRIBUTING.md#license) for details. diff --git a/SESSION-BRIEF.md b/SESSION-BRIEF.md index 03d6ffb..0029f62 100644 --- a/SESSION-BRIEF.md +++ b/SESSION-BRIEF.md @@ -1,129 +1,57 @@ # Session Brief: core/go-p2p -**Repo**: `forge.lthn.ai/core/go-p2p` (clone at `/tmp/core-go-p2p`) -**Module**: `forge.lthn.ai/core/go-p2p` -**Status**: 16 Go files, ~2,500 LOC, node tests PASS (42% coverage), ueps has NO TESTS -**Wiki**: https://forge.lthn.ai/core/go-p2p/wiki (6 pages) +**Repo**: `forge.lthn.ai/core/go-p2p` +**Module**: `dappco.re/go/core/p2p` +**Status**: `go build ./...`, `go vet ./...`, and `go test ./...` pass on 2026-03-27. +**Primary references**: `CLAUDE.md`, `docs/architecture.md`, `docs/development.md` ## What This Is -P2P networking layer for the Lethean network. Three packages: +P2P networking layer for the Lethean network. The repository currently consists of four Go packages: -### node/ — P2P Mesh (14 files) -- **Identity**: Ed25519 keypair generation, PEM serialisation, challenge-response auth -- **Transport**: Encrypted WebSocket connections via gorilla/websocket + Borg (encrypted blob storage) -- **Peers**: Registry with scoring, persistence, auth modes (open/allowlist), name validation -- **Messages**: Typed protocol messages (handshake, ping, stats, miner control, deploy, logs) -- **Protocol**: Response handler with validation and typed parsing -- **Worker**: Command handler (ping, stats, miner start/stop, deploy profiles, get logs) -- **Dispatcher**: UEPS packet routing skeleton with threat circuit breaker -- **Controller**: Remote node operations (connect, command, disconnect) -- **Bundle**: Service factory for Core framework DI registration - -### ueps/ — Wire Protocol (2 files, NO TESTS) -- **PacketBuilder**: Constructs signed UEPS frames with TLV encoding -- **ReadAndVerify**: Parses and verifies HMAC-SHA256 integrity -- TLV tags: 0x01-0x05 (header fields), 0x06 (HMAC), 0xFF (payload marker) -- Header: Version, CurrentLayer, TargetLayer, IntentID, ThreatScore - -### logging/ — Structured Logger (1 file) -- Simple levelled logger (INFO/WARN/ERROR/DEBUG) with key-value pairs +- `node/` — P2P mesh: identity, transport, peer registry, messages, protocol helpers, worker/controller logic, dispatcher, and deployment bundles +- `node/levin/` — standalone CryptoNote Levin binary protocol support +- `ueps/` — UEPS TLV wire protocol with HMAC-SHA256 integrity verification +- `logging/` — structured levelled logger with component scoping ## Current State | Area | Status | |------|--------| -| node/ tests | PASS — 42% statement coverage | -| ueps/ tests | NONE — zero test files | -| logging/ tests | NONE | -| go vet | Clean | -| TODOs/FIXMEs | None found | -| Identity (Ed25519) | Well tested — keypair, challenge-response, deterministic sigs | -| PeerRegistry | Well tested — add/remove, scoring, persistence, auth modes, name validation | -| Messages | Well tested — all 15 message types, serialisation, error codes | -| Worker | Well tested — ping, stats, miner, deploy, logs handlers | -| Transport | NOT tested — WebSocket + Borg encryption | -| Controller | NOT tested — remote node operations | -| Dispatcher | NOT tested — UEPS routing skeleton | +| Build | PASS | +| Vet | PASS | +| Tests | PASS | +| `logging/` | Has direct unit coverage | +| `ueps/` | Has round-trip, malformed packet, and coverage-path tests | +| `node/transport` | Has real WebSocket handshake and integration tests | +| `node/controller` | Has request/response, auto-connect, ping, and miner-control tests | +| `node/dispatcher` | Has routing, threshold, and concurrency tests | +| `node/levin` | Has protocol encode/decode coverage | + +## Key Behaviours + +- **Identity** — X25519 keypair generation via Borg STMF, persisted through XDG paths +- **Transport** — WebSocket mesh with challenge-response authentication, SMSG encryption, deduplication, rate limiting, and keepalive handling +- **Peer registry** — KD-tree selection across latency, hops, geography, and reliability score +- **Controller/worker** — request/response messaging for stats, miner control, logs, and deployment +- **Dispatcher** — UEPS intent routing with a threat circuit breaker at `ThreatScore > 50000` +- **Bundles** — TIM-based profile and miner bundle handling with defensive tar extraction ## Dependencies -- `github.com/Snider/Borg` v0.2.0 (encrypted blob storage) -- `github.com/Snider/Enchantrix` v0.0.2 (secure environment) -- `github.com/Snider/Poindexter` (secure pointer) -- `github.com/gorilla/websocket` v1.5.3 -- `github.com/google/uuid` v1.6.0 -- `github.com/ProtonMail/go-crypto` v1.3.0 +- `dappco.re/go/core` v0.8.0-alpha.1 +- `forge.lthn.ai/Snider/Borg` v0.3.1 +- `forge.lthn.ai/Snider/Poindexter` v0.0.3 - `github.com/adrg/xdg` v0.5.3 +- `github.com/google/uuid` v1.6.0 +- `github.com/gorilla/websocket` v1.5.3 - `github.com/stretchr/testify` v1.11.1 -- `golang.org/x/crypto` v0.45.0 - -## Priority Work - -### High (coverage gaps) -1. **UEPS tests** — Zero tests for the wire protocol. This is the consent-gated TLV protocol from RFC-021. Need: builder round-trip, HMAC verification, malformed packet rejection, boundary conditions (max ThreatScore, empty payload, oversized payload). -2. **Transport tests** — WebSocket connection, Borg encryption handshake, reconnection logic. -3. **Controller tests** — Connect/command/disconnect flow. -4. **Dispatcher tests** — UEPS routing, threat circuit breaker (ThreatScore > 50000 drops). - -### Medium (hardening) -5. **Increase node/ coverage** from 42% to 70%+ — focus on transport.go, controller.go, dispatcher.go -6. **Benchmarks** — Peer scoring, UEPS marshal/unmarshal, identity key generation -7. **Integration test** — Full node-to-node handshake over localhost WebSocket - -### Low (completeness) -8. **Logging tests** — Simple but should have coverage -9. **Peer discovery** — Currently manual. Add mDNS or DHT discovery -10. **Connection pooling** — Transport creates fresh connections; add pool for controller - -## File Map - -``` -/tmp/core-go-p2p/ -├── node/ -│ ├── bundle.go + bundle_test.go — Core DI factory -│ ├── identity.go + identity_test.go — Ed25519 keypair, PEM, challenge-response -│ ├── message.go + message_test.go — Protocol message types -│ ├── peer.go + peer_test.go — Registry, scoring, auth -│ ├── protocol.go + protocol_test.go — Response validation, typed parsing -│ ├── worker.go + worker_test.go — Command handlers -│ ├── transport.go (NO TEST) — WebSocket + Borg encryption -│ ├── controller.go (NO TEST) — Remote node operations -│ ├── dispatcher.go (NO TEST) — UEPS routing skeleton -│ └── logging.go — Package-level logger setup -├── ueps/ -│ ├── ueps.go (NO TEST) — PacketBuilder, ReadAndVerify, TLV -│ └── types.go (NO TEST) — UEPSHeader, ParsedPacket, intent IDs -├── logging/ -│ └── logger.go (NO TEST) — Levelled structured logger -├── go.mod -└── go.sum -``` - -## Key Interfaces - -```go -// node/message.go — 15 message types -const ( - MsgHandshake MsgHandshakeAck MsgPing MsgPong - MsgDisconnect MsgGetStats MsgStats MsgStartMiner - MsgStopMiner MsgMinerAck MsgDeploy MsgDeployAck - MsgGetLogs MsgLogs MsgError -) - -// ueps/types.go — UEPS header -type UEPSHeader struct { - Version uint8 // 0x09 - CurrentLayer uint8 - TargetLayer uint8 - IntentID uint8 // 0x01=Handshake, 0x20=Compute, 0x30=Rehab, 0xFF=Extended - ThreatScore uint16 -} -``` ## Conventions -- UK English -- Tests: testify assert/require -- Licence: EUPL-1.2 -- Lethean codenames: Borg (Secure/Blob), Poindexter (Secure/Pointer), Enchantrix (Secure/Environment) +- UK English in comments, logs, and docs +- `core.E()` for library error wrapping and sentinel definitions +- `core.Fs` adapters for library file I/O in `node/` +- `testify` in tests; prefer `t.Run()` tables for related cases +- EUPL-1.2 SPDX identifiers on new files +- Conventional commits with `Co-Authored-By: Virgil ` diff --git a/docs/architecture.md b/docs/architecture.md index 2915608..105c0ec 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture — go-p2p -`go-p2p` is the P2P networking layer for the Lethean network. Module path: `forge.lthn.ai/core/go-p2p`. +`go-p2p` is the P2P networking layer for the Lethean network. Module path: `dappco.re/go/core/p2p`. ## Package Structure @@ -17,7 +17,7 @@ go-p2p/ ### identity.go — Node Identity -Each node holds an Ed25519 keypair generated via Borg STMF (X25519 curve). The private key is stored at `~/.local/share/lethean-desktop/node/private.key` (mode 0600) and the public identity JSON at `~/.config/lethean-desktop/node.json`. +Each node holds an X25519 keypair generated via Borg STMF. The private key is stored at `~/.local/share/lethean-desktop/node/private.key` (mode 0600) and the public identity JSON at `~/.config/lethean-desktop/node.json`. `NodeIdentity` carries: - `ID` — 32-character hex string derived from SHA-256 of the public key (first 16 bytes) @@ -83,7 +83,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn `SelectOptimalPeer()` queries the tree for the point nearest to the origin (ideal: zero latency, zero hops, zero distance, maximum score). `SelectNearestPeers(n)` returns the n best. -**Persistence**: Writes are debounced with a 5-second coalesce window (`scheduleSave`). The actual write uses an atomic rename pattern (write to `.tmp`, then `os.Rename`) to prevent partial file corruption. `Close()` flushes any pending dirty state synchronously. +**Persistence**: Writes are debounced with a 5-second coalesce window (`scheduleSave`). The actual write uses an atomic rename pattern (write to `.tmp`, then rename) to prevent partial file corruption. `Close()` flushes any pending dirty state synchronously. **Auth modes**: - `PeerAuthOpen` — any connecting peer is accepted (default). @@ -209,10 +209,10 @@ The Unified Encrypted Packet Structure defines a TLV-encoded binary frame authen [0x04][len][IntentID] Header: Semantic routing token [0x05][0x02][ThreatScore] Header: uint16, big-endian [0x06][0x20][HMAC-SHA256] Signature: 32 bytes, covers header TLVs + payload data -[0xFF][...payload...] Data: no length prefix (relies on external framing) +[0xFF][len][...payload...] Data: length-prefixed payload ``` -**HMAC coverage**: The signature is computed over the serialised header TLVs (tags 0x01–0x05) concatenated with the raw payload bytes. The HMAC TLV itself (tag 0x06) and the payload tag byte (0xFF) are excluded from the signed data. +**HMAC coverage**: The signature is computed over the serialised header TLVs (tags 0x01–0x05) concatenated with the raw payload bytes. The HMAC TLV itself (tag 0x06) and the payload TLV header (tag `0xFF` plus the 2-byte length) are excluded from the signed data. ### PacketBuilder @@ -220,9 +220,7 @@ The Unified Encrypted Packet Structure defines a TLV-encoded binary frame authen ### ReadAndVerify -`ReadAndVerify(r *bufio.Reader, sharedSecret)` reads a stream, decodes the TLV fields in order, reconstructs the signed data buffer, and verifies the HMAC with `hmac.Equal`. Unknown TLV tags are accumulated into the signed data buffer (forward-compatible extension mechanism) but their semantics are ignored. - -**Known limitation**: Tag 0xFF carries no length prefix. The reader calls `io.ReadAll` on the remaining stream, which requires external TCP framing (e.g. a 4-byte length prefix on the enclosing connection) to delimit the packet boundary. The packet is not self-delimiting. +`ReadAndVerify(r *bufio.Reader, sharedSecret)` reads a stream, decodes the TLV fields in order, reconstructs the signed data buffer, and verifies the HMAC with `hmac.Equal`. Unknown TLV tags are accumulated into the signed data buffer (forward-compatible extension mechanism) but their semantics are ignored. The payload TLV is length-prefixed like every other field, so UEPS frames are self-delimiting. ## logging/ — Structured Logger @@ -255,8 +253,8 @@ The codebase is verified race-free under `go test -race`. ``` node/ ──► ueps/ node/ ──► logging/ -node/ ──► github.com/Snider/Borg (STMF crypto, SMSG encryption, TIM) -node/ ──► github.com/Snider/Poindexter (KD-tree peer selection) +node/ ──► forge.lthn.ai/Snider/Borg (STMF crypto, SMSG encryption, TIM) +node/ ──► forge.lthn.ai/Snider/Poindexter (KD-tree peer selection) node/ ──► github.com/gorilla/websocket node/ ──► github.com/google/uuid ueps/ ──► (stdlib only) diff --git a/docs/development.md b/docs/development.md index 42045f9..8d2bd3b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,7 +2,7 @@ ## Prerequisites -- Go 1.25 or later (the module declares `go 1.25.5`) +- Go 1.26 or later (the module declares `go 1.26.0`) - 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) @@ -43,7 +43,7 @@ go vet ./... ### 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. +Prefer table-driven subtests with `t.Run()` when multiple related cases share the same structure. Use clear case names and keep setup and verification consistent across the table. ```go func TestFoo(t *testing.T) { @@ -177,12 +177,12 @@ All parameters and return types must carry explicit type annotations. Avoid `int ### Error Handling - Never discard errors silently. -- Wrap errors with context using `fmt.Errorf("context: %w", err)`. +- Wrap library errors with context using `core.E("operation", "context", 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: +Every new file must carry the EUPL-1.2 licence identifier. The project is licensed under EUPL-1.2; do not include the full licence text in each file. A short SPDX identifier comment at the top is sufficient for new files: ```go // SPDX-License-Identifier: EUPL-1.2 diff --git a/docs/discovery.md b/docs/discovery.md index cc92c0d..a01f322 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -177,7 +177,7 @@ Peers are persisted to `~/.config/lethean-desktop/peers.json` as a JSON array. ### Debounced Writes -To avoid excessive disk I/O, saves are debounced with a 5-second coalesce interval. Multiple mutations within that window produce a single disk write. The write uses an atomic rename pattern (write to `.tmp`, then `os.Rename`) to prevent corruption on crash. +To avoid excessive disk I/O, saves are debounced with a 5-second coalesce interval. Multiple mutations within that window produce a single disk write. The write uses an atomic rename pattern (write to `.tmp`, then rename) to prevent corruption on crash. ```go // Flush pending changes on shutdown diff --git a/docs/history.md b/docs/history.md index 02f5819..52ea3f2 100644 --- a/docs/history.md +++ b/docs/history.md @@ -10,10 +10,10 @@ Implemented the complete test suite for the UEPS binary framing layer. Tests cov - PacketBuilder round-trip: basic, binary payload, elevated threat score, large payload - HMAC verification: payload tampering detected, header tampering detected, wrong shared secret detected -- Boundary conditions: nil payload, empty slice payload, `uint16` max ThreatScore (65,535), TLV value exceeding 255 bytes (`writeTLV` error path) +- Boundary conditions: nil payload, empty slice payload, `uint16` max ThreatScore (65,535), TLV value exceeding 65,535 bytes (`writeTLV` error path) - Stream robustness: truncated packets detected at multiple cut points (EOF mid-tag, mid-length, mid-value), missing HMAC tag, unknown TLV tags skipped and included in signed data -The 11.5% gap from 100% coverage is the reader's `io.ReadAll` error path, which requires a contrived broken `io.Reader` to exercise. +The remaining gap from 100% coverage at the time was the payload read-error path, which required a contrived broken reader to exercise. ### Phase 2 — Transport Tests @@ -86,11 +86,9 @@ Three integration tests (`TestIntegration_*`) exercise the full stack end-to-end ## Known Limitations -### UEPS 0xFF Payload Not Self-Delimiting +### UEPS Payload Framing (Resolved) -The `TagPayload` (0xFF) field carries no length prefix. `ReadAndVerify` calls `io.ReadAll` on the remaining stream, which means the packet format relies on external TCP framing to delimit the packet boundary. The enclosing transport must provide a length-prefixed frame before calling `ReadAndVerify`. This is noted in comments in both `packet.go` and `reader.go` but no solution is implemented. - -Consequence: UEPS packets cannot be chained in a raw stream without an outer framing protocol. The current WebSocket transport encapsulates each UEPS frame in a single WebSocket message, which provides the necessary boundary implicitly. +The `TagPayload` (0xFF) field now uses the same 2-byte length prefix as the other TLVs. `ReadAndVerify` reads that explicit length, so UEPS packets are self-delimiting and can be chained in a stream without relying on an outer framing layer. ### No Resource Cleanup on Some Error Paths diff --git a/docs/index.md b/docs/index.md index 3451618..4508eb8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ description: P2P mesh networking layer for the Lethean network. P2P networking layer for the Lethean network. Encrypted WebSocket mesh with UEPS wire protocol. -**Module:** `forge.lthn.ai/core/go-p2p` +**Module:** `dappco.re/go/core/p2p` **Go:** 1.26 **Licence:** EUPL-1.2 diff --git a/docs/routing.md b/docs/routing.md index b9536d5..2bb3405 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -5,7 +5,7 @@ description: UEPS intent-based packet routing with threat circuit breaker. # Intent Routing -The `Dispatcher` routes verified UEPS packets to registered intent handlers. Before routing, it enforces a threat circuit breaker that silently drops packets with elevated threat scores. +The `Dispatcher` routes verified UEPS packets to registered intent handlers. Before routing, it enforces a threat circuit breaker that blocks packets with elevated threat scores and returns sentinel errors to the caller. **File:** `node/dispatcher.go` @@ -74,8 +74,8 @@ Dropped packets are logged at WARN level with the threat score, threshold, inten ### Design Rationale -- **High-threat packets are dropped silently** (from the sender's perspective) rather than returning an error, consistent with the "don't even parse the payload" philosophy. -- **Unknown intents are dropped**, not forwarded, to avoid back-pressure on the transport layer. They are logged at WARN level for debugging. +- **High-threat packets are not dispatched**. The dispatcher logs them and returns `ErrThreatScoreExceeded` to the caller; the sender still receives no protocol-level response. +- **Unknown intents are not forwarded**. The dispatcher logs them and returns `ErrUnknownIntent`, avoiding back-pressure on the transport layer. - **Handler errors propagate** to the caller, allowing upstream code to record failures. ## Intent Constants @@ -100,12 +100,13 @@ const ( ```go var ( - ErrThreatScoreExceeded = fmt.Errorf( - "packet rejected: threat score exceeds safety threshold (%d)", - ThreatScoreThreshold, + ErrThreatScoreExceeded = core.E( + "Dispatcher.Dispatch", + core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), + nil, ) - ErrUnknownIntent = errors.New("packet dropped: unknown intent") - ErrNilPacket = errors.New("dispatch: nil packet") + ErrUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) + ErrNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) ``` diff --git a/docs/ueps.md b/docs/ueps.md index c86b498..2ba184a 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -7,7 +7,7 @@ description: TLV-encoded wire protocol with HMAC-SHA256 integrity verification ( The `ueps` package implements the Universal Encrypted Payload System -- a consent-gated TLV (Type-Length-Value) wire protocol with HMAC-SHA256 integrity verification. This is the low-level binary protocol that sits beneath the JSON-over-WebSocket mesh layer. -**Package:** `forge.lthn.ai/core/go-p2p/ueps` +**Package:** `dappco.re/go/core/p2p/ueps` ## TLV Format -- 2.45.3 From 82d425d01e467d268ed5d35999bca1574e0903e1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 20:28:53 +0000 Subject: [PATCH 06/60] docs(repo): populate package specs Co-Authored-By: Virgil --- specs/logging.md | 80 +++++++++++++++ specs/node-levin.md | 117 ++++++++++++++++++++++ specs/node.md | 237 ++++++++++++++++++++++++++++++++++++++++++++ specs/ueps.md | 67 +++++++++++++ 4 files changed, 501 insertions(+) create mode 100644 specs/logging.md create mode 100644 specs/node-levin.md create mode 100644 specs/node.md create mode 100644 specs/ueps.md diff --git a/specs/logging.md b/specs/logging.md new file mode 100644 index 0000000..67c4fde --- /dev/null +++ b/specs/logging.md @@ -0,0 +1,80 @@ +# logging + +**Import:** `dappco.re/go/core/p2p/logging` + +**Files:** 1 + +## Types + +### `Level` +`type Level int` + +Log severity used by `Logger`. `String` renders the level name in upper case, and `ParseLevel` accepts `debug`, `info`, `warn` or `warning`, and `error`. + +### `Config` +```go +type Config struct { + Output io.Writer + Level Level + Component string +} +``` + +Configuration passed to `New`. + +- `Output`: destination for log lines. `New` falls back to stderr when this is `nil`. +- `Level`: minimum severity that will be emitted. +- `Component`: optional component label added to each line. + +### `Fields` +`type Fields map[string]any` + +Structured key/value fields passed to logging calls. When multiple `Fields` values are supplied, they are merged from left to right, so later maps override earlier keys. + +### `Logger` +`type Logger struct { /* unexported fields */ }` + +Structured logger with configurable output, severity filtering, and component scoping. Log writes are serialised by a mutex and are formatted as timestamped single-line records. + +## Functions + +### Top-level + +| Name | Signature | Description | +| --- | --- | --- | +| `DefaultConfig` | `func DefaultConfig() Config` | Returns the default configuration: stderr output, `LevelInfo`, and no component label. | +| `New` | `func New(cfg Config) *Logger` | Creates a `Logger` from `cfg`, substituting the default stderr writer when `cfg.Output` is `nil`. | +| `SetGlobal` | `func SetGlobal(l *Logger)` | Replaces the package-level global logger instance. | +| `GetGlobal` | `func GetGlobal() *Logger` | Returns the current package-level global logger. | +| `SetGlobalLevel` | `func SetGlobalLevel(level Level)` | Updates the minimum severity on the current global logger. | +| `Debug` | `func Debug(msg string, fields ...Fields)` | Logs a debug message through the global logger. | +| `Info` | `func Info(msg string, fields ...Fields)` | Logs an informational message through the global logger. | +| `Warn` | `func Warn(msg string, fields ...Fields)` | Logs a warning message through the global logger. | +| `Error` | `func Error(msg string, fields ...Fields)` | Logs an error message through the global logger. | +| `Debugf` | `func Debugf(format string, args ...any)` | Formats and logs a debug message through the global logger. | +| `Infof` | `func Infof(format string, args ...any)` | Formats and logs an informational message through the global logger. | +| `Warnf` | `func Warnf(format string, args ...any)` | Formats and logs a warning message through the global logger. | +| `Errorf` | `func Errorf(format string, args ...any)` | Formats and logs an error message through the global logger. | +| `ParseLevel` | `func ParseLevel(s string) (Level, error)` | Parses a text level into `Level`. Unknown strings return `LevelInfo` plus an error. | + +### `Level` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `String` | `func (l Level) String() string` | Returns `DEBUG`, `INFO`, `WARN`, `ERROR`, or `UNKNOWN` for out-of-range values. | + +### `*Logger` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Returns a new logger that uses the same output and current level but replaces the component label. | +| `SetLevel` | `func (l *Logger) SetLevel(level Level)` | Sets the minimum severity that the logger will emit. | +| `GetLevel` | `func (l *Logger) GetLevel() Level` | Returns the current minimum severity. | +| `Debug` | `func (l *Logger) Debug(msg string, fields ...Fields)` | Logs `msg` at debug level after merging any supplied field maps. | +| `Info` | `func (l *Logger) Info(msg string, fields ...Fields)` | Logs `msg` at info level after merging any supplied field maps. | +| `Warn` | `func (l *Logger) Warn(msg string, fields ...Fields)` | Logs `msg` at warning level after merging any supplied field maps. | +| `Error` | `func (l *Logger) Error(msg string, fields ...Fields)` | Logs `msg` at error level after merging any supplied field maps. | +| `Debugf` | `func (l *Logger) Debugf(format string, args ...any)` | Formats and logs a debug message. | +| `Infof` | `func (l *Logger) Infof(format string, args ...any)` | Formats and logs an informational message. | +| `Warnf` | `func (l *Logger) Warnf(format string, args ...any)` | Formats and logs a warning message. | +| `Errorf` | `func (l *Logger) Errorf(format string, args ...any)` | Formats and logs an error message. | diff --git a/specs/node-levin.md b/specs/node-levin.md new file mode 100644 index 0000000..b5e8912 --- /dev/null +++ b/specs/node-levin.md @@ -0,0 +1,117 @@ +# levin + +**Import:** `dappco.re/go/core/p2p/node/levin` + +**Files:** 4 + +## Types + +### `Connection` +```go +type Connection struct { + MaxPayloadSize uint64 + ReadTimeout time.Duration + WriteTimeout time.Duration +} +``` + +Wrapper around `net.Conn` that reads and writes framed Levin packets. + +- `MaxPayloadSize`: per-connection payload ceiling enforced by `ReadPacket`. `NewConnection` starts with the package `MaxPayloadSize` default. +- `ReadTimeout`: deadline applied before each `ReadPacket` call. `NewConnection` sets this to `DefaultReadTimeout`. +- `WriteTimeout`: deadline applied before each write. `NewConnection` sets this to `DefaultWriteTimeout`. + +### `Header` +```go +type Header struct { + Signature uint64 + PayloadSize uint64 + ExpectResponse bool + Command uint32 + ReturnCode int32 + Flags uint32 + ProtocolVersion uint32 +} +``` + +Packed 33-byte Levin frame header. `EncodeHeader` writes these fields little-endian, and `DecodeHeader` validates the `Signature` and package-level `MaxPayloadSize`. + +### `Section` +`type Section map[string]Value` + +Portable-storage object used by the Levin encoder and decoder. `EncodeStorage` sorts keys alphabetically for deterministic output. + +### `Value` +```go +type Value struct { + Type uint8 +} +``` + +Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Val`, `StringVal`, and `ObjectArrayVal` create correctly-typed instances. + +## Functions + +### Top-level framing and storage functions + +| Name | Signature | Description | +| --- | --- | --- | +| `NewConnection` | `func NewConnection(conn net.Conn) *Connection` | Wraps `conn` with Levin defaults: 100 MB payload limit, 120 s read timeout, and 30 s write timeout. | +| `EncodeHeader` | `func EncodeHeader(h *Header) [HeaderSize]byte` | Serialises `h` into the fixed 33-byte Levin header format. | +| `DecodeHeader` | `func DecodeHeader(buf [HeaderSize]byte) (Header, error)` | Parses a 33-byte header, rejecting bad magic signatures and payload sizes above the package-level limit. | +| `PackVarint` | `func PackVarint(v uint64) []byte` | Encodes `v` using the epee portable-storage varint scheme where the low two bits of the first byte encode the width. | +| `UnpackVarint` | `func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error)` | Decodes one portable-storage varint and returns the value, consumed width, and any truncation or overflow error. | +| `EncodeStorage` | `func EncodeStorage(s Section) ([]byte, error)` | Serialises a `Section` into portable-storage binary form, including the 9-byte storage header. | +| `DecodeStorage` | `func DecodeStorage(data []byte) (Section, error)` | Deserialises portable-storage binary data, validates the storage signatures and version, and reconstructs a `Section`. | + +### `Value` constructors + +| Name | Signature | Description | +| --- | --- | --- | +| `Uint64Val` | `func Uint64Val(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | +| `Uint32Val` | `func Uint32Val(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | +| `Uint16Val` | `func Uint16Val(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | +| `Uint8Val` | `func Uint8Val(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | +| `Int64Val` | `func Int64Val(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | +| `Int32Val` | `func Int32Val(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | +| `Int16Val` | `func Int16Val(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | +| `Int8Val` | `func Int8Val(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | +| `BoolVal` | `func BoolVal(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | +| `DoubleVal` | `func DoubleVal(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | +| `StringVal` | `func StringVal(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | +| `ObjectVal` | `func ObjectVal(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | +| `Uint64ArrayVal` | `func Uint64ArrayVal(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | +| `Uint32ArrayVal` | `func Uint32ArrayVal(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | +| `StringArrayVal` | `func StringArrayVal(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | +| `ObjectArrayVal` | `func ObjectArrayVal(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | + +### `*Connection` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `WritePacket` | `func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error` | Sends a Levin request or notification with `FlagRequest`, `ReturnOK`, and the current protocol version. Header and payload writes are serialised by an internal mutex. | +| `WriteResponse` | `func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error` | Sends a Levin response with `FlagResponse` and the supplied return code. | +| `ReadPacket` | `func (c *Connection) ReadPacket() (Header, []byte, error)` | Applies the read deadline, reads exactly one header and payload, validates the frame, and enforces the connection-specific `MaxPayloadSize`. Empty payloads are returned as `nil` without allocation. | +| `Close` | `func (c *Connection) Close() error` | Closes the wrapped network connection. | +| `RemoteAddr` | `func (c *Connection) RemoteAddr() string` | Returns the wrapped connection's remote address string. | + +### `Value` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `AsUint64` | `func (v Value) AsUint64() (uint64, error)` | Returns the scalar `uint64` value or `ErrStorageTypeMismatch`. | +| `AsUint32` | `func (v Value) AsUint32() (uint32, error)` | Returns the scalar `uint32` value or `ErrStorageTypeMismatch`. | +| `AsUint16` | `func (v Value) AsUint16() (uint16, error)` | Returns the scalar `uint16` value or `ErrStorageTypeMismatch`. | +| `AsUint8` | `func (v Value) AsUint8() (uint8, error)` | Returns the scalar `uint8` value or `ErrStorageTypeMismatch`. | +| `AsInt64` | `func (v Value) AsInt64() (int64, error)` | Returns the scalar `int64` value or `ErrStorageTypeMismatch`. | +| `AsInt32` | `func (v Value) AsInt32() (int32, error)` | Returns the scalar `int32` value or `ErrStorageTypeMismatch`. | +| `AsInt16` | `func (v Value) AsInt16() (int16, error)` | Returns the scalar `int16` value or `ErrStorageTypeMismatch`. | +| `AsInt8` | `func (v Value) AsInt8() (int8, error)` | Returns the scalar `int8` value or `ErrStorageTypeMismatch`. | +| `AsBool` | `func (v Value) AsBool() (bool, error)` | Returns the scalar `bool` value or `ErrStorageTypeMismatch`. | +| `AsDouble` | `func (v Value) AsDouble() (float64, error)` | Returns the scalar `float64` value or `ErrStorageTypeMismatch`. | +| `AsString` | `func (v Value) AsString() ([]byte, error)` | Returns the scalar byte-string or `ErrStorageTypeMismatch`. | +| `AsSection` | `func (v Value) AsSection() (Section, error)` | Returns the nested `Section` or `ErrStorageTypeMismatch`. | +| `AsUint64Array` | `func (v Value) AsUint64Array() ([]uint64, error)` | Returns the `[]uint64` array or `ErrStorageTypeMismatch`. | +| `AsUint32Array` | `func (v Value) AsUint32Array() ([]uint32, error)` | Returns the `[]uint32` array or `ErrStorageTypeMismatch`. | +| `AsStringArray` | `func (v Value) AsStringArray() ([][]byte, error)` | Returns the `[][]byte` array or `ErrStorageTypeMismatch`. | +| `AsSectionArray` | `func (v Value) AsSectionArray() ([]Section, error)` | Returns the `[]Section` array or `ErrStorageTypeMismatch`. | diff --git a/specs/node.md b/specs/node.md new file mode 100644 index 0000000..d78cb03 --- /dev/null +++ b/specs/node.md @@ -0,0 +1,237 @@ +# node + +**Import:** `dappco.re/go/core/p2p/node` + +**Files:** 12 + +## Types + +### Core types + +| Type | Definition | Description | +| --- | --- | --- | +| `BundleType` | `type BundleType string` | Deployment bundle kind used by `Bundle` and `BundleManifest`. | +| `Bundle` | `struct{ Type BundleType; Name string; Data []byte; Checksum string }` | Transferable deployment bundle. `Data` contains STIM-encrypted bytes or raw JSON, and `Checksum` is the SHA-256 hex digest of `Data`. | +| `BundleManifest` | `struct{ Type BundleType; Name string; Version string; MinerType string; ProfileIDs []string; CreatedAt string }` | Metadata describing the logical contents of a bundle payload. | +| `Controller` | `struct{ /* unexported fields */ }` | High-level controller client for remote peer operations. It keeps a pending-response map keyed by request ID and registers its internal response handler with the transport in `NewController`. | +| `Dispatcher` | `struct{ /* unexported fields */ }` | Concurrent-safe UEPS router. It applies the threat-score circuit breaker before dispatching to a handler map keyed by `IntentID`. | +| `IntentHandler` | `type IntentHandler func(pkt *ueps.ParsedPacket) error` | Callback signature used by `Dispatcher` for verified UEPS packets. | +| `Message` | `struct{ ID string; Type MessageType; From string; To string; Timestamp time.Time; Payload RawMessage; ReplyTo string }` | Generic P2P message envelope. `Payload` stores raw JSON, and `ReplyTo` links responses back to the originating request. | +| `MessageDeduplicator` | `struct{ /* unexported fields */ }` | TTL cache of recently seen message IDs used to suppress duplicates. | +| `MessageHandler` | `type MessageHandler func(conn *PeerConnection, msg *Message)` | Callback signature for decrypted inbound transport messages. | +| `MessageType` | `type MessageType string` | String message discriminator stored in `Message.Type`. | +| `NodeIdentity` | `struct{ ID string; Name string; PublicKey string; CreatedAt time.Time; Role NodeRole }` | Public node identity. `ID` is derived from the first 16 bytes of the SHA-256 hash of the public key. | +| `NodeManager` | `struct{ /* unexported fields */ }` | Identity and key manager that loads, generates, persists, and deletes X25519 node credentials. | +| `NodeRole` | `type NodeRole string` | Operational mode string for controller, worker, or dual-role nodes. | +| `Peer` | `struct{ ID string; Name string; PublicKey string; Address string; Role NodeRole; AddedAt time.Time; LastSeen time.Time; PingMS float64; Hops int; GeoKM float64; Score float64; Connected bool }` | Registry record for a remote node, including addressing, role, scoring metrics, and transient connection state. | +| `PeerAuthMode` | `type PeerAuthMode int` | Peer admission policy used by `PeerRegistry` when unknown peers attempt to connect. | +| `PeerConnection` | `struct{ Peer *Peer; Conn *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | +| `PeerRateLimiter` | `struct{ /* unexported fields */ }` | Per-peer token bucket limiter used by the transport hot path. | +| `PeerRegistry` | `struct{ /* unexported fields */ }` | Concurrent peer store with KD-tree selection, allowlist state, and debounced persistence to disk. | +| `ProtocolError` | `struct{ Code int; Message string }` | Structured remote error returned by protocol response helpers when a peer replies with `MsgError`. | +| `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | +| `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | +| `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | +| `TransportConfig` | `struct{ ListenAddr string; WSPath string; TLSCertPath string; TLSKeyPath string; MaxConns int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | + +### Payload and integration types + +| Type | Definition | Description | +| --- | --- | --- | +| `DeployAckPayload` | `struct{ Success bool; Name string; Error string }` | Deployment acknowledgement with success state, optional deployed name, and optional error text. | +| `DeployPayload` | `struct{ BundleType string; Data []byte; Checksum string; Name string }` | Deployment request carrying STIM-encrypted bundle bytes (or other bundle data), checksum, and logical name. | +| `DisconnectPayload` | `struct{ Reason string; Code int }` | Disconnect notice with human-readable reason and optional disconnect code. | +| `ErrorPayload` | `struct{ Code int; Message string; Details string }` | Payload used by `MsgError` responses. | +| `GetLogsPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | +| `HandshakeAckPayload` | `struct{ Identity NodeIdentity; ChallengeResponse []byte; Accepted bool; Reason string }` | Handshake reply containing the responder identity, optional challenge response, acceptance flag, and optional rejection reason. | +| `HandshakePayload` | `struct{ Identity NodeIdentity; Challenge []byte; Version string }` | Handshake request containing node identity, optional authentication challenge, and protocol version. | +| `LogsPayload` | `struct{ MinerName string; Lines []string; HasMore bool }` | Returned miner log lines plus an indicator that more lines are available. | +| `MinerAckPayload` | `struct{ Success bool; MinerName string; Error string }` | Acknowledgement for remote miner start and stop operations. | +| `MinerInstance` | `interface{ GetName() string; GetType() string; GetStats() (any, error); GetConsoleHistory(lines int) []string }` | Minimal runtime miner contract used by the worker to collect stats and logs without importing the mining package. | +| `MinerManager` | `interface{ StartMiner(minerType string, config any) (MinerInstance, error); StopMiner(name string) error; ListMiners() []MinerInstance; GetMiner(name string) (MinerInstance, error) }` | Worker-facing miner control contract. | +| `MinerStatsItem` | `struct{ Name string; Type string; Hashrate float64; Shares int; Rejected int; Uptime int; Pool string; Algorithm string; CPUThreads int }` | Protocol-facing summary of one miner's runtime statistics. | +| `PingPayload` | `struct{ SentAt int64 }` | Ping payload carrying the sender's millisecond timestamp. | +| `PongPayload` | `struct{ SentAt int64; ReceivedAt int64 }` | Ping response carrying the echoed send time and the receiver's millisecond timestamp. | +| `ProfileManager` | `interface{ GetProfile(id string) (any, error); SaveProfile(profile any) error }` | Worker-facing profile storage contract. | +| `StartMinerPayload` | `struct{ MinerType string; ProfileID string; Config RawMessage }` | Request to start a miner with an optional profile ID and raw JSON config override. | +| `StatsPayload` | `struct{ NodeID string; NodeName string; Miners []MinerStatsItem; Uptime int64 }` | Node-wide stats response with node identity fields, miner summaries, and uptime in seconds. | +| `StopMinerPayload` | `struct{ MinerName string }` | Request to stop a miner by name. | + +## Functions + +### Bundle, protocol, and utility functions + +| Name | Signature | Description | +| --- | --- | --- | +| `CreateProfileBundle` | `func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error)` | Builds a TIM containing `profileJSON`, encrypts it to STIM with `password`, and returns a `BundleProfile` bundle with a SHA-256 checksum. | +| `CreateProfileBundleUnencrypted` | `func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error)` | Returns a `BundleProfile` bundle whose `Data` is the raw JSON payload and whose checksum is computed over that JSON. | +| `CreateMinerBundle` | `func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error)` | Reads a miner binary, tars it, loads it into a TIM, optionally attaches `profileJSON`, encrypts the result to STIM, and returns a `BundleMiner` bundle. | +| `ExtractProfileBundle` | `func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error)` | Verifies `bundle.Checksum`, returns raw JSON directly when `bundle.Data` already looks like JSON, otherwise decrypts STIM and returns the embedded config bytes. | +| `ExtractMinerBundle` | `func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error)` | Verifies checksum, decrypts STIM, extracts the root filesystem tarball into `destDir`, and returns the first executable path plus the embedded config bytes. | +| `VerifyBundle` | `func VerifyBundle(bundle *Bundle) bool` | Returns whether `bundle.Checksum` matches the SHA-256 checksum of `bundle.Data`. | +| `StreamBundle` | `func StreamBundle(bundle *Bundle, w io.Writer) error` | JSON-encodes `bundle` and writes it to `w`. | +| `ReadBundle` | `func ReadBundle(r io.Reader) (*Bundle, error)` | Reads all bytes from `r`, JSON-decodes them into a `Bundle`, and returns the result. | +| `GenerateChallenge` | `func GenerateChallenge() ([]byte, error)` | Returns a new 32-byte random authentication challenge. | +| `SignChallenge` | `func SignChallenge(challenge []byte, sharedSecret []byte) []byte` | Computes the HMAC-SHA256 signature of `challenge` using `sharedSecret`. | +| `VerifyChallenge` | `func VerifyChallenge(challenge, response, sharedSecret []byte) bool` | Recomputes the expected challenge signature and compares it to `response` with `hmac.Equal`. | +| `IsProtocolVersionSupported` | `func IsProtocolVersionSupported(version string) bool` | Returns whether `version` is present in `SupportedProtocolVersions`. | +| `MarshalJSON` | `func MarshalJSON(v any) ([]byte, error)` | Encodes `v` with the core JSON helper, restores the package's historical no-EscapeHTML behaviour, and returns a caller-owned copy of the bytes. | +| `NewMessage` | `func NewMessage(msgType MessageType, from, to string, payload any) (*Message, error)` | Creates a message with a generated UUID, current timestamp, and JSON-encoded payload. A `nil` payload leaves `Payload` empty. | +| `NewErrorMessage` | `func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error)` | Creates a `MsgError` response containing an `ErrorPayload` and sets `ReplyTo` to the supplied request ID. | +| `ValidateResponse` | `func ValidateResponse(resp *Message, expectedType MessageType) error` | Convenience wrapper that delegates to `DefaultResponseHandler.ValidateResponse`. | +| `ParseResponse` | `func ParseResponse(resp *Message, expectedType MessageType, target any) error` | Convenience wrapper that delegates to `DefaultResponseHandler.ParseResponse`. | +| `IsProtocolError` | `func IsProtocolError(err error) bool` | Returns whether `err` is a `*ProtocolError`. | +| `GetProtocolErrorCode` | `func GetProtocolErrorCode(err error) int` | Returns `err.(*ProtocolError).Code` when `err` is a `*ProtocolError`, otherwise `0`. | + +### Constructors + +| Name | Signature | Description | +| --- | --- | --- | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `:9091`, `/ws`, `MaxConns=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | +| `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | +| `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | +| `NewNodeManager` | `func NewNodeManager() (*NodeManager, error)` | Resolves XDG key and config paths, then loads an existing identity if present. | +| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager using explicit key and config paths, primarily for tests. | +| `NewPeerRateLimiter` | `func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter` | Creates a token bucket seeded with `maxTokens` and refilled at `refillRate` tokens per second. | +| `NewPeerRegistry` | `func NewPeerRegistry() (*PeerRegistry, error)` | Resolves the XDG peers path, loads any persisted peers, and builds the selection KD-tree. | +| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | +| `NewTransport` | `func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport` | Creates a transport with lifecycle context, a 5-minute message deduplicator, and a WebSocket upgrader that only accepts local origins. | +| `NewWorker` | `func NewWorker(node *NodeManager, transport *Transport) *Worker` | Creates a worker, records its start time for uptime reporting, and defaults `DataDir` to `xdg.DataHome`. | + +### `RawMessage` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `MarshalJSON` | `func (m RawMessage) MarshalJSON() ([]byte, error)` | Emits raw payload bytes unchanged, or `null` when the receiver is `nil`. | +| `UnmarshalJSON` | `func (m *RawMessage) UnmarshalJSON(data []byte) error` | Copies `data` into the receiver without decoding it. Passing a `nil` receiver returns an error. | + +### `*Message` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `Reply` | `func (m *Message) Reply(msgType MessageType, payload any) (*Message, error)` | Creates a reply message that swaps `From` and `To` and sets `ReplyTo` to `m.ID`. | +| `ParsePayload` | `func (m *Message) ParsePayload(v any) error` | JSON-decodes `Payload` into `v`. A `nil` payload is treated as a no-op. | + +### `*NodeManager` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `HasIdentity` | `func (n *NodeManager) HasIdentity() bool` | Returns whether an identity is currently loaded in memory. | +| `GetIdentity` | `func (n *NodeManager) GetIdentity() *NodeIdentity` | Returns a copy of the loaded public identity, or `nil` when no identity is initialised. | +| `GenerateIdentity` | `func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error` | Generates a new X25519 keypair, derives the node ID from the public key hash, stores the public identity, and persists both key and config to disk. | +| `DeriveSharedSecret` | `func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error)` | Decodes the peer public key, performs X25519 ECDH with the node private key, hashes the result with SHA-256, and returns the symmetric key material. | +| `Delete` | `func (n *NodeManager) Delete() error` | Removes persisted key/config files when they exist and clears the in-memory identity and key state. | + +### `*Controller` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `GetRemoteStats` | `func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error)` | Sends `MsgGetStats` to `peerID`, waits for a response, and decodes the resulting `MsgStats` payload. | +| `StartRemoteMiner` | `func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error` | Validates `minerType`, sends `MsgStartMiner`, waits for `MsgMinerAck`, and returns an error when the remote ack reports failure. | +| `StopRemoteMiner` | `func (c *Controller) StopRemoteMiner(peerID, minerName string) error` | Sends `MsgStopMiner`, waits for `MsgMinerAck`, and returns an error when the remote ack reports failure. | +| `GetRemoteLogs` | `func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error)` | Requests `MsgLogs` from a remote miner and returns the decoded log lines. | +| `GetAllStats` | `func (c *Controller) GetAllStats() map[string]*StatsPayload` | Requests stats from every currently connected peer and returns the successful responses keyed by peer ID. | +| `PingPeer` | `func (c *Controller) PingPeer(peerID string) (float64, error)` | Sends a ping, measures round-trip time in milliseconds, and updates the peer registry metrics for that peer. | +| `ConnectToPeer` | `func (c *Controller) ConnectToPeer(peerID string) error` | Looks up `peerID` in the registry and establishes a transport connection. | +| `DisconnectFromPeer` | `func (c *Controller) DisconnectFromPeer(peerID string) error` | Gracefully closes an active transport connection for `peerID`. | + +### `*Dispatcher` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `RegisterHandler` | `func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler)` | Associates `handler` with `intentID`, replacing any existing handler for that intent. | +| `Handlers` | `func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler]` | Returns an iterator over the currently registered intent handlers. | +| `Dispatch` | `func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error` | Rejects `nil` packets, drops packets whose `ThreatScore` exceeds `ThreatScoreThreshold`, rejects unknown intents, and otherwise invokes the matching handler. | + +### `*MessageDeduplicator` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `IsDuplicate` | `func (d *MessageDeduplicator) IsDuplicate(msgID string) bool` | Returns whether `msgID` is still present in the deduplicator's TTL window. | +| `Mark` | `func (d *MessageDeduplicator) Mark(msgID string)` | Records `msgID` with the current time. | +| `Cleanup` | `func (d *MessageDeduplicator) Cleanup()` | Removes expired message IDs whose age exceeds the configured TTL. | + +### `*PeerRateLimiter` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `Allow` | `func (r *PeerRateLimiter) Allow() bool` | Refills tokens according to elapsed whole seconds and returns whether one token could be consumed for the current message. | + +### `*PeerRegistry` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `SetAuthMode` | `func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode)` | Replaces the current peer admission mode. | +| `GetAuthMode` | `func (r *PeerRegistry) GetAuthMode() PeerAuthMode` | Returns the current peer admission mode. | +| `AllowPublicKey` | `func (r *PeerRegistry) AllowPublicKey(publicKey string)` | Adds `publicKey` to the explicit allowlist. | +| `RevokePublicKey` | `func (r *PeerRegistry) RevokePublicKey(publicKey string)` | Removes `publicKey` from the explicit allowlist. | +| `IsPublicKeyAllowed` | `func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool` | Returns whether `publicKey` is currently allowlisted. | +| `IsPeerAllowed` | `func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool` | Returns `true` in open mode, or in allowlist mode when the peer is already registered or the supplied public key is allowlisted. | +| `ListAllowedPublicKeys` | `func (r *PeerRegistry) ListAllowedPublicKeys() []string` | Returns a slice snapshot of allowlisted public keys. | +| `AllowedPublicKeys` | `func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string]` | Returns an iterator over allowlisted public keys. | +| `AddPeer` | `func (r *PeerRegistry) AddPeer(peer *Peer) error` | Validates the peer, sets `AddedAt` when zero, defaults `Score` to `50`, adds it to the registry, rebuilds the KD-tree, and schedules a debounced save. | +| `UpdatePeer` | `func (r *PeerRegistry) UpdatePeer(peer *Peer) error` | Replaces an existing peer entry, rebuilds the KD-tree, and schedules a debounced save. | +| `RemovePeer` | `func (r *PeerRegistry) RemovePeer(id string) error` | Deletes an existing peer, rebuilds the KD-tree, and schedules a debounced save. | +| `GetPeer` | `func (r *PeerRegistry) GetPeer(id string) *Peer` | Returns a copy of the peer identified by `id`, or `nil` when absent. | +| `ListPeers` | `func (r *PeerRegistry) ListPeers() []*Peer` | Returns a slice of peer copies. | +| `Peers` | `func (r *PeerRegistry) Peers() iter.Seq[*Peer]` | Returns an iterator over peer copies so callers cannot mutate registry state directly. | +| `UpdateMetrics` | `func (r *PeerRegistry) UpdateMetrics(id string, pingMS, geoKM float64, hops int) error` | Updates latency, distance, hop count, and `LastSeen`, rebuilds the KD-tree, and schedules a debounced save. | +| `UpdateScore` | `func (r *PeerRegistry) UpdateScore(id string, score float64) error` | Clamps `score` into `[0,100]`, updates the peer, rebuilds the KD-tree, and schedules a debounced save. | +| `SetConnected` | `func (r *PeerRegistry) SetConnected(id string, connected bool)` | Updates the connection flag for a peer and refreshes `LastSeen` when marking the peer connected. | +| `RecordSuccess` | `func (r *PeerRegistry) RecordSuccess(id string)` | Increases the peer score by `ScoreSuccessIncrement` up to `ScoreMaximum`, updates `LastSeen`, and schedules a save. | +| `RecordFailure` | `func (r *PeerRegistry) RecordFailure(id string)` | Decreases the peer score by `ScoreFailureDecrement` down to `ScoreMinimum` and schedules a save. | +| `RecordTimeout` | `func (r *PeerRegistry) RecordTimeout(id string)` | Decreases the peer score by `ScoreTimeoutDecrement` down to `ScoreMinimum` and schedules a save. | +| `GetPeersByScore` | `func (r *PeerRegistry) GetPeersByScore() []*Peer` | Returns peers sorted by descending score. | +| `PeersByScore` | `func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer]` | Returns an iterator over peers sorted by descending score. | +| `SelectOptimalPeer` | `func (r *PeerRegistry) SelectOptimalPeer() *Peer` | Uses the KD-tree to find the peer closest to the ideal metrics vector and returns a copy of that peer. | +| `SelectNearestPeers` | `func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer` | Returns copies of the `n` nearest peers from the KD-tree according to the weighted metrics. | +| `GetConnectedPeers` | `func (r *PeerRegistry) GetConnectedPeers() []*Peer` | Returns a slice of copies for peers whose `Connected` flag is true. | +| `ConnectedPeers` | `func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer]` | Returns an iterator over connected peer copies. | +| `Count` | `func (r *PeerRegistry) Count() int` | Returns the number of registered peers. | +| `Close` | `func (r *PeerRegistry) Close() error` | Stops any pending save timer and immediately flushes dirty peer data to disk when needed. | + +### `*ResponseHandler` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `ValidateResponse` | `func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error` | Rejects `nil` responses, unwraps `MsgError` into a `ProtocolError`, and checks that `resp.Type` matches `expectedType`. | +| `ParseResponse` | `func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error` | Runs `ValidateResponse` and then decodes the payload into `target` when `target` is not `nil`. | + +### `*Transport` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `Start` | `func (t *Transport) Start() error` | Starts the WebSocket listener and begins accepting inbound peer connections. | +| `Stop` | `func (t *Transport) Stop() error` | Cancels transport context, closes active connections, and shuts down the listener. | +| `OnMessage` | `func (t *Transport) OnMessage(handler MessageHandler)` | Installs the inbound message callback used after decryption. It must be set before `Start` to avoid races. | +| `Connect` | `func (t *Transport) Connect(peer *Peer) (*PeerConnection, error)` | Dials `peer`, performs the handshake, derives the shared secret, and returns the active peer connection. | +| `Send` | `func (t *Transport) Send(peerID string, msg *Message) error` | Looks up the active connection for `peerID` and sends `msg` over it. | +| `Connections` | `func (t *Transport) Connections() iter.Seq[*PeerConnection]` | Returns an iterator over active peer connections. | +| `Broadcast` | `func (t *Transport) Broadcast(msg *Message) error` | Sends `msg` to every connected peer except the sender identified by `msg.From`. | +| `GetConnection` | `func (t *Transport) GetConnection(peerID string) *PeerConnection` | Returns the active connection for `peerID`, or `nil` when not connected. | +| `ConnectedPeers` | `func (t *Transport) ConnectedPeers() int` | Returns the number of active peer connections. | + +### `*PeerConnection` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `Send` | `func (pc *PeerConnection) Send(msg *Message) error` | Encrypts and writes a message over the WebSocket connection. | +| `Close` | `func (pc *PeerConnection) Close() error` | Closes the underlying connection once and releases transport state for that peer. | +| `GracefulClose` | `func (pc *PeerConnection) GracefulClose(reason string, code int) error` | Sends a `MsgDisconnect` notification before closing the connection. | + +### `*Worker` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `SetMinerManager` | `func (w *Worker) SetMinerManager(manager MinerManager)` | Installs the miner manager used for start, stop, stats, and log requests. | +| `SetProfileManager` | `func (w *Worker) SetProfileManager(manager ProfileManager)` | Installs the profile manager used during deployment handling. | +| `HandleMessage` | `func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message)` | Dispatches supported message types, sends normal replies on success, and emits `MsgError` responses when a handled command fails. | +| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | + +### `*ProtocolError` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `Error` | `func (e *ProtocolError) Error() string` | Formats the remote error as `remote error (): `. | diff --git a/specs/ueps.md b/specs/ueps.md new file mode 100644 index 0000000..0be6353 --- /dev/null +++ b/specs/ueps.md @@ -0,0 +1,67 @@ +# ueps + +**Import:** `dappco.re/go/core/p2p/ueps` + +**Files:** 2 + +## Types + +### `UEPSHeader` +```go +type UEPSHeader struct { + Version uint8 + CurrentLayer uint8 + TargetLayer uint8 + IntentID uint8 + ThreatScore uint16 +} +``` + +Routing and integrity metadata carried in UEPS frames. + +- `Version`: protocol version byte. `NewBuilder` initialises this to `0x09`. +- `CurrentLayer`: source layer byte. `NewBuilder` initialises this to `5`. +- `TargetLayer`: destination layer byte. `NewBuilder` initialises this to `5`. +- `IntentID`: semantic intent token. +- `ThreatScore`: unsigned 16-bit risk score. + +### `PacketBuilder` +```go +type PacketBuilder struct { + Header UEPSHeader + Payload []byte +} +``` + +Mutable packet assembly state used to produce a signed UEPS frame. + +- `Header`: TLV metadata written before the payload. +- `Payload`: raw payload bytes appended as the terminal TLV. + +### `ParsedPacket` +```go +type ParsedPacket struct { + Header UEPSHeader + Payload []byte +} +``` + +Verified packet returned by `ReadAndVerify`. + +- `Header`: decoded UEPS header values reconstructed from the stream. +- `Payload`: payload bytes from the `TagPayload` TLV. + +## Functions + +### Top-level + +| Name | Signature | Description | +| --- | --- | --- | +| `NewBuilder` | `func NewBuilder(intentID uint8, payload []byte) *PacketBuilder` | Creates a packet builder with default header values (`Version=0x09`, `CurrentLayer=5`, `TargetLayer=5`, `ThreatScore=0`) and the supplied intent and payload. | +| `ReadAndVerify` | `func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error)` | Reads TLVs from `r` until `TagPayload`, reconstructs the signed header bytes, and verifies the HMAC-SHA256 over headers plus payload using `sharedSecret`. Missing signatures, truncated data, and HMAC mismatches return errors. | + +### `*PacketBuilder` methods + +| Name | Signature | Description | +| --- | --- | --- | +| `MarshalAndSign` | `func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error)` | Serialises header TLVs `0x01` through `0x05`, signs those bytes plus `Payload` with HMAC-SHA256, appends the `TagHMAC` TLV, then writes the terminal `TagPayload` TLV. All TLV lengths are encoded as 2-byte big-endian unsigned integers. | -- 2.45.3 From ddb6df55c5b81d02f858e07235f035d6cecd9f77 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 30 Mar 2026 20:27:11 +0100 Subject: [PATCH 07/60] docs: add AX design principles RFC for agent dispatch Co-Authored-By: Virgil --- docs/RFC-CORE-008-AGENT-EXPERIENCE.md | 440 ++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 docs/RFC-CORE-008-AGENT-EXPERIENCE.md diff --git a/docs/RFC-CORE-008-AGENT-EXPERIENCE.md b/docs/RFC-CORE-008-AGENT-EXPERIENCE.md new file mode 100644 index 0000000..3763521 --- /dev/null +++ b/docs/RFC-CORE-008-AGENT-EXPERIENCE.md @@ -0,0 +1,440 @@ +# RFC-025: Agent Experience (AX) Design Principles + +- **Status:** Draft +- **Authors:** Snider, Cladius +- **Date:** 2026-03-19 +- **Applies to:** All Core ecosystem packages (CoreGO, CorePHP, CoreTS, core-agent) + +## Abstract + +Agent Experience (AX) is a design paradigm for software systems where the primary code consumer is an AI agent, not a human developer. AX sits alongside User Experience (UX) and Developer Experience (DX) as the third era of interface design. + +This RFC establishes AX as a formal design principle for the Core ecosystem and defines the conventions that follow from it. + +## Motivation + +As of early 2026, AI agents write, review, and maintain the majority of code in the Core ecosystem. The original author has not manually edited code (outside of Core struct design) since October 2025. Code is processed semantically — agents reason about intent, not characters. + +Design patterns inherited from the human-developer era optimise for the wrong consumer: + +- **Short names** save keystrokes but increase semantic ambiguity +- **Functional option chains** are fluent for humans but opaque for agents tracing configuration +- **Error-at-every-call-site** produces 50% boilerplate that obscures intent +- **Generic type parameters** force agents to carry type context that the runtime already has +- **Panic-hiding conventions** (`Must*`) create implicit control flow that agents must special-case + +AX acknowledges this shift and provides principles for designing code, APIs, file structures, and conventions that serve AI agents as first-class consumers. + +## The Three Eras + +| Era | Primary Consumer | Optimises For | Key Metric | +|-----|-----------------|---------------|------------| +| UX | End users | Discoverability, forgiveness, visual clarity | Task completion time | +| DX | Developers | Typing speed, IDE support, convention familiarity | Time to first commit | +| AX | AI agents | Predictability, composability, semantic navigation | Correct-on-first-pass rate | + +AX does not replace UX or DX. End users still need good UX. Developers still need good DX. But when the primary code author and maintainer is an AI agent, the codebase should be designed for that consumer first. + +## Principles + +### 1. Predictable Names Over Short Names + +Names are tokens that agents pattern-match across languages and contexts. Abbreviations introduce mapping overhead. + +``` +Config not Cfg +Service not Srv +Embed not Emb +Error not Err (as a subsystem name; err for local variables is fine) +Options not Opts +``` + +**Rule:** If a name would require a comment to explain, it is too short. + +**Exception:** Industry-standard abbreviations that are universally understood (`HTTP`, `URL`, `ID`, `IPC`, `I18n`) are acceptable. The test: would an agent trained on any mainstream language recognise it without context? + +### 2. Comments as Usage Examples + +The function signature tells WHAT. The comment shows HOW with real values. + +```go +// Detect the project type from files present +setup.Detect("/path/to/project") + +// Set up a workspace with auto-detected template +setup.Run(setup.Options{Path: ".", Template: "auto"}) + +// Scaffold a PHP module workspace +setup.Run(setup.Options{Path: "./my-module", Template: "php"}) +``` + +**Rule:** If a comment restates what the type signature already says, delete it. If a comment shows a concrete usage with realistic values, keep it. + +**Rationale:** Agents learn from examples more effectively than from descriptions. A comment like "Run executes the setup process" adds zero information. A comment like `setup.Run(setup.Options{Path: ".", Template: "auto"})` teaches an agent exactly how to call the function. + +### 3. Path Is Documentation + +File and directory paths should be self-describing. An agent navigating the filesystem should understand what it is looking at without reading a README. + +``` +flow/deploy/to/homelab.yaml — deploy TO the homelab +flow/deploy/from/github.yaml — deploy FROM GitHub +flow/code/review.yaml — code review flow +template/file/go/struct.go.tmpl — Go struct file template +template/dir/workspace/php/ — PHP workspace scaffold +``` + +**Rule:** If an agent needs to read a file to understand what a directory contains, the directory naming has failed. + +**Corollary:** The unified path convention (folder structure = HTTP route = CLI command = test path) is AX-native. One path, every surface. + +### 4. Templates Over Freeform + +When an agent generates code from a template, the output is constrained to known-good shapes. When an agent writes freeform, the output varies. + +```go +// Template-driven — consistent output +lib.RenderFile("php/action", data) +lib.ExtractDir("php", targetDir, data) + +// Freeform — variance in output +"write a PHP action class that..." +``` + +**Rule:** For any code pattern that recurs, provide a template. Templates are guardrails for agents. + +**Scope:** Templates apply to file generation, workspace scaffolding, config generation, and commit messages. They do NOT apply to novel logic — agents should write business logic freeform with the domain knowledge available. + +### 5. Declarative Over Imperative + +Agents reason better about declarations of intent than sequences of operations. + +```yaml +# Declarative — agent sees what should happen +steps: + - name: build + flow: tools/docker-build + with: + context: "{{ .app_dir }}" + image_name: "{{ .image_name }}" + + - name: deploy + flow: deploy/with/docker + with: + host: "{{ .host }}" +``` + +```go +// Imperative — agent must trace execution +cmd := exec.Command("docker", "build", "--platform", "linux/amd64", "-t", imageName, ".") +cmd.Dir = appDir +if err := cmd.Run(); err != nil { + return fmt.Errorf("docker build: %w", err) +} +``` + +**Rule:** Orchestration, configuration, and pipeline logic should be declarative (YAML/JSON). Implementation logic should be imperative (Go/PHP/TS). The boundary is: if an agent needs to compose or modify the logic, make it declarative. + +### 6. Universal Types (Core Primitives) + +Every component in the ecosystem accepts and returns the same primitive types. An agent processing any level of the tree sees identical shapes. + +```go +// Universal contract +setup.Run(core.Options{Path: ".", Template: "auto"}) +brain.New(core.Options{Name: "openbrain"}) +deploy.Run(core.Options{Flow: "deploy/to/homelab"}) + +// Fractal — Core itself is a Service +core.New(core.Options{ + Services: []core.Service{ + process.New(core.Options{Name: "process"}), + brain.New(core.Options{Name: "brain"}), + }, +}) +``` + +**Core primitive types:** + +| Type | Purpose | +|------|---------| +| `core.Options` | Input configuration (what you want) | +| `core.Config` | Runtime settings (what is active) | +| `core.Data` | Embedded or stored content | +| `core.Service` | A managed component with lifecycle | +| `core.Result[T]` | Return value with OK/fail state | + +**What this replaces:** + +| Go Convention | Core AX | Why | +|--------------|---------|-----| +| `func With*(v) Option` | `core.Options{Field: v}` | Struct literal is parseable; option chain requires tracing | +| `func Must*(v) T` | `core.Result[T]` | No hidden panics; errors flow through Core | +| `func *For[T](c) T` | `c.Service("name")` | String lookup is greppable; generics require type context | +| `val, err :=` everywhere | Single return via `core.Result` | Intent not obscured by error handling | +| `_ = err` | Never needed | Core handles all errors internally | + +### 7. Directory as Semantics + +The directory structure tells an agent the intent before it reads a word. Top-level directories are semantic categories, not organisational bins. + +``` +plans/ +├── code/ # Pure primitives — read for WHAT exists +├── project/ # Products — read for WHAT we're building and WHY +└── rfc/ # Contracts — read for constraints and rules +``` + +**Rule:** An agent should know what kind of document it's reading from the path alone. `code/core/go/io/RFC.md` = a lib primitive spec. `project/ofm/RFC.md` = a product spec that cross-references code/. `rfc/snider/borg/RFC-BORG-006-SMSG-FORMAT.md` = an immutable contract for the Borg SMSG protocol. + +**Corollary:** The three-way split (code/project/rfc) extends principle 3 (Path Is Documentation) from files to entire subtrees. The path IS the metadata. + +### 8. Lib Never Imports Consumer + +Dependency flows one direction. Libraries define primitives. Consumers compose from them. A new feature in a consumer can never break a library. + +``` +code/core/go/* → lib tier (stable foundation) +code/core/agent/ → consumer tier (composes from go/*) +code/core/cli/ → consumer tier (composes from go/*) +code/core/gui/ → consumer tier (composes from go/*) +``` + +**Rule:** If package A is in `go/` and package B is in the consumer tier, B may import A but A must never import B. The repo naming convention enforces this: `go-{name}` = lib, bare `{name}` = consumer. + +**Why this matters for agents:** When an agent is dispatched to implement a feature in `core/agent`, it can freely import from `go-io`, `go-scm`, `go-process`. But if an agent is dispatched to `go-io`, it knows its changes are foundational — every consumer depends on it, so the contract must not break. + +### 9. Issues Are N+(rounds) Deep + +Problems in code and specs are layered. Surface issues mask deeper issues. Fixing the surface reveals the next layer. This is not a failure mode — it is the discovery process. + +``` +Pass 1: Find 16 issues (surface — naming, imports, obvious errors) +Pass 2: Find 11 issues (structural — contradictions, missing types) +Pass 3: Find 5 issues (architectural — signature mismatches, registration gaps) +Pass 4: Find 4 issues (contract — cross-spec API mismatches) +Pass 5: Find 2 issues (mechanical — path format, nil safety) +Pass N: Findings are trivial → spec/code is complete +``` + +**Rule:** Iteration is required, not a failure. Each pass sees what the previous pass could not, because the context changed. An agent dispatched with the same task on the same repo will find different things each time — this is correct behaviour. + +**Corollary:** The cheapest model should do the most passes (surface work). The frontier model should arrive last, when only deep issues remain. Tiered iteration: grunt model grinds → mid model pre-warms → frontier model polishes. + +**Anti-pattern:** One-shot generation expecting valid output. No model, no human, produces correct-on-first-pass for non-trivial work. Expecting it wastes the first pass on surface issues that a cheaper pass would have caught. + +### 10. CLI Tests as Artifact Validation + +Unit tests verify the code. CLI tests verify the binary. The directory structure IS the command structure — path maps to command, Taskfile runs the test. + +``` +tests/cli/ +├── core/ +│ └── lint/ +│ ├── Taskfile.yaml ← test `core-lint` (root) +│ ├── run/ +│ │ ├── Taskfile.yaml ← test `core-lint run` +│ │ └── fixtures/ +│ ├── go/ +│ │ ├── Taskfile.yaml ← test `core-lint go` +│ │ └── fixtures/ +│ └── security/ +│ ├── Taskfile.yaml ← test `core-lint security` +│ └── fixtures/ +``` + +**Rule:** Every CLI command has a matching `tests/cli/{path}/Taskfile.yaml`. The Taskfile runs the compiled binary against fixtures with known inputs and validates the output. If the CLI test passes, the underlying actions work — because CLI commands call actions, MCP tools call actions, API endpoints call actions. Test the CLI, trust the rest. + +**Pattern:** + +```yaml +# tests/cli/core/lint/go/Taskfile.yaml +version: '3' +tasks: + test: + cmds: + - core-lint go --output json fixtures/ > /tmp/result.json + - jq -e '.findings | length > 0' /tmp/result.json + - jq -e '.summary.passed == false' /tmp/result.json +``` + +**Why this matters for agents:** An agent can validate its own work by running `task test` in the matching `tests/cli/` directory. No test framework, no mocking, no setup — just the binary, fixtures, and `jq` assertions. The agent builds the binary, runs the test, sees the result. If it fails, the agent can read the fixture, read the output, and fix the code. + +**Corollary:** Fixtures are planted bugs. Each fixture file has a known issue that the linter must find. If the linter doesn't find it, the test fails. Fixtures are the spec for what the tool must detect — they ARE the test cases, not descriptions of test cases. + +## Applying AX to Existing Patterns + +### File Structure + +``` +# AX-native: path describes content +core/agent/ +├── go/ # Go source +├── php/ # PHP source +├── ui/ # Frontend source +├── claude/ # Claude Code plugin +└── codex/ # Codex plugin + +# Not AX: generic names requiring README +src/ +├── lib/ +├── utils/ +└── helpers/ +``` + +### Error Handling + +```go +// AX-native: errors are infrastructure, not application logic +svc := c.Service("brain") +cfg := c.Config().Get("database.host") +// Errors logged by Core. Code reads like a spec. + +// Not AX: errors dominate the code +svc, err := c.ServiceFor[brain.Service]() +if err != nil { + return fmt.Errorf("get brain service: %w", err) +} +cfg, err := c.Config().Get("database.host") +if err != nil { + _ = err // silenced because "it'll be fine" +} +``` + +### API Design + +```go +// AX-native: one shape, every surface +core.New(core.Options{ + Name: "my-app", + Services: []core.Service{...}, + Config: core.Config{...}, +}) + +// Not AX: multiple patterns for the same thing +core.New( + core.WithName("my-app"), + core.WithService(factory1), + core.WithService(factory2), + core.WithConfig(cfg), +) +``` + +## The Plans Convention — AX Development Lifecycle + +The `plans/` directory structure encodes a development methodology designed for how generative AI actually works: iterative refinement across structured phases, not one-shot generation. + +### The Three-Way Split + +``` +plans/ +├── project/ # 1. WHAT and WHY — start here +├── rfc/ # 2. CONSTRAINTS — immutable contracts +└── code/ # 3. HOW — implementation specs +``` + +Each directory is a phase. Work flows from project → rfc → code. Each transition forces a refinement pass — you cannot write a code spec without discovering gaps in the project spec, and you cannot write an RFC without discovering assumptions in both. + +**Three places for data that can't be written simultaneously = three guaranteed iterations of "actually, this needs changing."** Refinement is baked into the structure, not bolted on as a review step. + +### Phase 1: Project (Vision) + +Start with `project/`. No code exists yet. Define: +- What the product IS and who it serves +- What existing primitives it consumes (cross-ref to `code/`) +- What constraints it operates under (cross-ref to `rfc/`) + +This is where creativity lives. Map features to building blocks. Connect systems. The project spec is integrative — it references everything else. + +### Phase 2: RFC (Contracts) + +Extract the immutable rules into `rfc/`. These are constraints that don't change with implementation: +- Wire formats, protocols, hash algorithms +- Security properties that must hold +- Compatibility guarantees + +RFCs are numbered per component (`RFC-BORG-006-SMSG-FORMAT.md`) and never modified after acceptance. If the contract changes, write a new RFC. + +### Phase 3: Code (Implementation Specs) + +Define the implementation in `code/`. Each component gets an RFC.md that an agent can implement from: +- Struct definitions (the DTOs — see principle 6) +- Method signatures and behaviour +- Error conditions and edge cases +- Cross-references to other code/ specs + +The code spec IS the product. Write the spec → dispatch to an agent → review output → iterate. + +### Pre-Launch: Alignment Protocol + +Before dispatching for implementation, verify spec-model alignment: + +``` +1. REVIEW — The implementation model (Codex/Jules) reads the spec + and reports missing elements. This surfaces the delta between + the model's training and the spec's assumptions. + + "I need X, Y, Z to implement this" is the model saying + "I hear you but I'm missing context" — without asking. + +2. ADJUST — Update the spec to close the gaps. Add examples, + clarify ambiguities, provide the context the model needs. + This is shared alignment, not compromise. + +3. VERIFY — A different model (or sub-agent) reviews the adjusted + spec without the planner's bias. Fresh eyes on the contract. + "Does this make sense to someone who wasn't in the room?" + +4. READY — When the review findings are trivial or deployment- + related (not architectural), the spec is ready to dispatch. +``` + +### Implementation: Iterative Dispatch + +Same prompt, multiple runs. Each pass sees deeper because the context evolved: + +``` +Round 1: Build features (the obvious gaps) +Round 2: Write tests (verify what was built) +Round 3: Harden security (what can go wrong?) +Round 4: Next RFC section (what's still missing?) +Round N: Findings are trivial → implementation is complete +``` + +Re-running is not failure. It is the process. Each pass changes the codebase, which changes what the next pass can see. The iteration IS the refinement. + +### Post-Implementation: Auto-Documentation + +The QA/verify chain produces artefacts that feed forward: +- Test results document the contract (what works, what doesn't) +- Coverage reports surface untested paths +- Diff summaries prep the changelog for the next release +- Doc site updates from the spec (the spec IS the documentation) + +The output of one cycle is the input to the next. The plans repo stays current because the specs drive the code, not the other way round. + +## Compatibility + +AX conventions are valid, idiomatic Go/PHP/TS. They do not require language extensions, code generation, or non-standard tooling. An AX-designed codebase compiles, tests, and deploys with standard toolchains. + +The conventions diverge from community patterns (functional options, Must/For, etc.) but do not violate language specifications. This is a style choice, not a fork. + +## Adoption + +AX applies to all new code in the Core ecosystem. Existing code migrates incrementally as it is touched — no big-bang rewrite. + +Priority order: +1. **Public APIs** (package-level functions, struct constructors) +2. **File structure** (path naming, template locations) +3. **Internal fields** (struct field names, local variables) + +## References + +- dAppServer unified path convention (2024) +- CoreGO DTO pattern refactor (2026-03-18) +- Core primitives design (2026-03-19) +- Go Proverbs, Rob Pike (2015) — AX provides an updated lens + +## Changelog + +- 2026-03-19: Initial draft -- 2.45.3 From 2d63a8ba18e253a3957338f2c4b667b4cd885c32 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 19:32:26 +0000 Subject: [PATCH 08/60] refactor(node): add AX-native aliases for component and path APIs Co-Authored-By: Virgil --- docs/discovery.md | 2 +- docs/identity.md | 2 +- logging/logger.go | 11 +++++++++-- logging/logger_test.go | 11 ++++++++--- node/identity.go | 14 +++++++++----- node/peer.go | 14 +++++++++----- node/worker.go | 11 +++++++++-- specs/logging.md | 3 ++- specs/node.md | 9 ++++++--- 9 files changed, 54 insertions(+), 23 deletions(-) diff --git a/docs/discovery.md b/docs/discovery.md index a01f322..3423165 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -146,7 +146,7 @@ This also updates `LastSeen` and triggers a KD-tree rebuild. ```go // Create registry, err := node.NewPeerRegistry() // XDG paths -registry, err := node.NewPeerRegistryWithPath(path) // Custom path (testing) +registry, err := node.NewPeerRegistryFromPath(path) // Custom path (testing) // CRUD err := registry.AddPeer(peer) diff --git a/docs/identity.md b/docs/identity.md index c16a559..b102683 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -53,7 +53,7 @@ Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce ### Custom Paths (Testing) ```go -nm, err := node.NewNodeManagerWithPaths( +nm, err := node.NewNodeManagerFromPaths( "/tmp/test/private.key", "/tmp/test/node.json", ) diff --git a/logging/logger.go b/logging/logger.go index d162458..739b54d 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -87,8 +87,10 @@ func New(cfg Config) *Logger { } } -// WithComponent returns a new Logger with the specified component name. -func (l *Logger) WithComponent(component string) *Logger { +// ComponentLogger returns a new Logger scoped to one component. +// +// transportLogger := logger.ComponentLogger("transport") +func (l *Logger) ComponentLogger(component string) *Logger { return &Logger{ output: l.output, level: l.level, @@ -96,6 +98,11 @@ func (l *Logger) WithComponent(component string) *Logger { } } +// Deprecated: use ComponentLogger. +func (l *Logger) WithComponent(component string) *Logger { + return l.ComponentLogger(component) +} + // SetLevel sets the minimum log level. func (l *Logger) SetLevel(level Level) { l.mu.Lock() diff --git a/logging/logger_test.go b/logging/logger_test.go index 38f8cb9..0850cde 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -75,7 +75,7 @@ func TestLogger_WithFields_Good(t *testing.T) { } } -func TestLogger_WithComponent_Good(t *testing.T) { +func TestLogger_ConfigComponent_Good(t *testing.T) { var buf bytes.Buffer logger := New(Config{ Output: &buf, @@ -91,20 +91,25 @@ func TestLogger_WithComponent_Good(t *testing.T) { } } -func TestLogger_DerivedComponent_Good(t *testing.T) { +func TestLogger_ComponentLogger_Good(t *testing.T) { var buf bytes.Buffer parent := New(Config{ Output: &buf, Level: LevelInfo, }) - child := parent.WithComponent("ChildComponent") + child := parent.ComponentLogger("ChildComponent") child.Info("child message") + alias := parent.WithComponent("AliasComponent") + alias.Info("alias message") output := buf.String() if !core.Contains(output, "[ChildComponent]") { t.Error("Derived component name should appear") } + if !core.Contains(output, "[AliasComponent]") { + t.Error("Compatibility alias should preserve the component name") + } } func TestLogger_Formatted_Good(t *testing.T) { diff --git a/node/identity.go b/node/identity.go index 5ec6a88..a6cb702 100644 --- a/node/identity.go +++ b/node/identity.go @@ -99,14 +99,13 @@ func NewNodeManager() (*NodeManager, error) { return nil, core.E("NodeManager.New", "failed to get config path", err) } - return NewNodeManagerWithPaths(keyPath, configPath) + return NewNodeManagerFromPaths(keyPath, configPath) } -// NewNodeManagerWithPaths creates a NodeManager with custom paths. -// This is primarily useful for testing to avoid xdg path caching issues. +// NewNodeManagerFromPaths creates a NodeManager using explicit key and config paths. // -// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { +// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, configPath: configPath, @@ -121,6 +120,11 @@ func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } +// Deprecated: use NewNodeManagerFromPaths. +func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { + return NewNodeManagerFromPaths(keyPath, configPath) +} + // HasIdentity returns true if a node identity has been initialized. func (n *NodeManager) HasIdentity() bool { n.mu.RLock() diff --git a/node/peer.go b/node/peer.go index 19ce7a9..ea96695 100644 --- a/node/peer.go +++ b/node/peer.go @@ -131,14 +131,13 @@ func NewPeerRegistry() (*PeerRegistry, error) { return nil, core.E("PeerRegistry.New", "failed to get peers path", err) } - return NewPeerRegistryWithPath(peersPath) + return NewPeerRegistryFromPath(peersPath) } -// NewPeerRegistryWithPath creates a new PeerRegistry with a custom path. -// This is primarily useful for testing to avoid xdg path caching issues. +// NewPeerRegistryFromPath creates a PeerRegistry using an explicit peers file path. // -// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { +// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), path: peersPath, @@ -158,6 +157,11 @@ func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { return pr, nil } +// Deprecated: use NewPeerRegistryFromPath. +func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { + return NewPeerRegistryFromPath(peersPath) +} + // SetAuthMode sets the authentication mode for peer connections. func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { r.allowedPublicKeyMu.Lock() diff --git a/node/worker.go b/node/worker.go index 0f97063..23d3178 100644 --- a/node/worker.go +++ b/node/worker.go @@ -407,7 +407,14 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err } } -// RegisterWithTransport registers the worker's message handler with the transport. -func (w *Worker) RegisterWithTransport() { +// RegisterOnTransport installs the worker message handler on the transport. +// +// worker.RegisterOnTransport() +func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } + +// Deprecated: use RegisterOnTransport. +func (w *Worker) RegisterWithTransport() { + w.RegisterOnTransport() +} diff --git a/specs/logging.md b/specs/logging.md index 67c4fde..31de890 100644 --- a/specs/logging.md +++ b/specs/logging.md @@ -67,7 +67,8 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | -| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Returns a new logger that uses the same output and current level but replaces the component label. | +| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. Preferred over `WithComponent`. | +| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Deprecated compatibility alias for `ComponentLogger`. | | `SetLevel` | `func (l *Logger) SetLevel(level Level)` | Sets the minimum severity that the logger will emit. | | `GetLevel` | `func (l *Logger) GetLevel() Level` | Returns the current minimum severity. | | `Debug` | `func (l *Logger) Debug(msg string, fields ...Fields)` | Logs `msg` at debug level after merging any supplied field maps. | diff --git a/specs/node.md b/specs/node.md index d78cb03..87324b3 100644 --- a/specs/node.md +++ b/specs/node.md @@ -93,10 +93,12 @@ | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | | `NewNodeManager` | `func NewNodeManager() (*NodeManager, error)` | Resolves XDG key and config paths, then loads an existing identity if present. | -| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager using explicit key and config paths, primarily for tests. | +| `NewNodeManagerFromPaths` | `func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager from explicit key and config paths. | +| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Deprecated compatibility alias for `NewNodeManagerFromPaths`. | | `NewPeerRateLimiter` | `func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter` | Creates a token bucket seeded with `maxTokens` and refilled at `refillRate` tokens per second. | | `NewPeerRegistry` | `func NewPeerRegistry() (*PeerRegistry, error)` | Resolves the XDG peers path, loads any persisted peers, and builds the selection KD-tree. | -| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | +| `NewPeerRegistryFromPath` | `func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | +| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Deprecated compatibility alias for `NewPeerRegistryFromPath`. | | `NewTransport` | `func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport` | Creates a transport with lifecycle context, a 5-minute message deduplicator, and a WebSocket upgrader that only accepts local origins. | | `NewWorker` | `func NewWorker(node *NodeManager, transport *Transport) *Worker` | Creates a worker, records its start time for uptime reporting, and defaults `DataDir` to `xdg.DataHome`. | @@ -228,7 +230,8 @@ | `SetMinerManager` | `func (w *Worker) SetMinerManager(manager MinerManager)` | Installs the miner manager used for start, stop, stats, and log requests. | | `SetProfileManager` | `func (w *Worker) SetProfileManager(manager ProfileManager)` | Installs the profile manager used during deployment handling. | | `HandleMessage` | `func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message)` | Dispatches supported message types, sends normal replies on success, and emits `MsgError` responses when a handled command fails. | -| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | +| `RegisterOnTransport` | `func (w *Worker) RegisterOnTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | +| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Deprecated compatibility alias for `RegisterOnTransport`. | ### `*ProtocolError` methods -- 2.45.3 From c678d2060851c6b7756943e3419dbbabbd730677 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 19:44:05 +0000 Subject: [PATCH 09/60] refactor(repo): prefer AX error names Co-Authored-By: Virgil --- docs/architecture.md | 6 +-- docs/history.md | 4 +- docs/routing.md | 20 +++---- node/controller.go | 10 ++-- node/dispatcher.go | 33 +++++++----- node/errors.go | 16 ++++-- node/identity.go | 2 +- node/levin/connection.go | 2 +- node/levin/header.go | 31 ++++++++--- node/levin/storage.go | 112 +++++++++++++++++++++++---------------- node/levin/varint.go | 24 +++++---- node/message.go | 42 ++++++++++----- node/protocol.go | 4 +- node/transport.go | 2 +- node/worker.go | 10 ++-- specs/node-levin.md | 32 +++++------ 16 files changed, 211 insertions(+), 139 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 105c0ec..49f8af1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -183,9 +183,9 @@ Auto-connect: if the target peer is not yet connected, `sendRequest` calls `tran | `IntentCustom` | `0xFF` | Application-level sub-protocols | **Sentinel errors**: -- `ErrThreatScoreExceeded` — threat circuit breaker fired -- `ErrUnknownIntent` — no handler registered for the `IntentID` -- `ErrNilPacket` — nil packet passed to `Dispatch` +- `ErrorThreatScoreExceeded` — threat circuit breaker fired +- `ErrorUnknownIntent` — no handler registered for the `IntentID` +- `ErrorNilPacket` — nil packet passed to `Dispatch` ### bundle.go — TIM Deployment Bundles diff --git a/docs/history.md b/docs/history.md index 52ea3f2..369614a 100644 --- a/docs/history.md +++ b/docs/history.md @@ -57,8 +57,8 @@ Replaced the dispatcher stub with a complete implementation. 10 test functions, Design decisions recorded at the time: 1. `IntentHandler` as a `func` type rather than an interface, matching the `MessageHandler` pattern already used in `transport.go`. Lighter weight for a single-method contract. -2. Sentinel errors (`ErrThreatScoreExceeded`, `ErrUnknownIntent`, `ErrNilPacket`) rather than silent drops. Callers can inspect outcomes; the dispatcher still logs at WARN level regardless. -3. Threat check occurs before intent routing. A high-threat packet with an unknown intent returns `ErrThreatScoreExceeded`, not `ErrUnknownIntent`. The circuit breaker is the first line of defence. +2. Sentinel errors (`ErrorThreatScoreExceeded`, `ErrorUnknownIntent`, `ErrorNilPacket`) rather than silent drops. Callers can inspect outcomes; the dispatcher still logs at WARN level regardless. +3. Threat check occurs before intent routing. A high-threat packet with an unknown intent returns `ErrorThreatScoreExceeded`, not `ErrorUnknownIntent`. The circuit breaker is the first line of defence. 4. Threshold fixed at 50,000 (a constant, not configurable) to match the original stub specification. The value sits at approximately 76% of `uint16` max. 5. `sync.RWMutex` for the handler map. Registration is infrequent (write lock); dispatch is read-heavy (read lock). diff --git a/docs/routing.md b/docs/routing.md index 2bb3405..9fc6e82 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -57,9 +57,9 @@ err := dispatcher.Dispatch(packet) ### Dispatch Flow -1. **Nil check** -- returns `ErrNilPacket` immediately. -2. **Threat circuit breaker** -- if `ThreatScore > 50,000`, the packet is dropped and `ErrThreatScoreExceeded` is returned. A warning is logged. -3. **Intent lookup** -- finds the handler registered for `pkt.Header.IntentID`. If none exists, the packet is dropped and `ErrUnknownIntent` is returned. +1. **Nil check** -- returns `ErrorNilPacket` immediately. +2. **Threat circuit breaker** -- if `ThreatScore > 50,000`, the packet is dropped and `ErrorThreatScoreExceeded` is returned. A warning is logged. +3. **Intent lookup** -- finds the handler registered for `pkt.Header.IntentID`. If none exists, the packet is dropped and `ErrorUnknownIntent` is returned. 4. **Handler invocation** -- calls the handler and returns its result. ### Threat Circuit Breaker @@ -74,8 +74,8 @@ Dropped packets are logged at WARN level with the threat score, threshold, inten ### Design Rationale -- **High-threat packets are not dispatched**. The dispatcher logs them and returns `ErrThreatScoreExceeded` to the caller; the sender still receives no protocol-level response. -- **Unknown intents are not forwarded**. The dispatcher logs them and returns `ErrUnknownIntent`, avoiding back-pressure on the transport layer. +- **High-threat packets are not dispatched**. The dispatcher logs them and returns `ErrorThreatScoreExceeded` to the caller; the sender still receives no protocol-level response. +- **Unknown intents are not forwarded**. The dispatcher logs them and returns `ErrorUnknownIntent`, avoiding back-pressure on the transport layer. - **Handler errors propagate** to the caller, allowing upstream code to record failures. ## Intent Constants @@ -100,13 +100,13 @@ const ( ```go var ( - ErrThreatScoreExceeded = core.E( + ErrorThreatScoreExceeded = core.E( "Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil, ) - ErrUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - ErrNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) + ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) + ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) ``` @@ -124,11 +124,11 @@ if err != nil { // Route through the dispatcher if err := dispatcher.Dispatch(packet); err != nil { - if errors.Is(err, node.ErrThreatScoreExceeded) { + if errors.Is(err, node.ErrorThreatScoreExceeded) { // Packet was too threatening -- already logged return nil } - if errors.Is(err, node.ErrUnknownIntent) { + if errors.Is(err, node.ErrorUnknownIntent) { // No handler for this intent -- already logged return nil } diff --git a/node/controller.go b/node/controller.go index 5724b9a..90e9085 100644 --- a/node/controller.go +++ b/node/controller.go @@ -118,7 +118,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { identity := c.node.GetIdentity() if identity == nil { - return nil, ErrIdentityNotInitialized + return nil, ErrorIdentityNotInitialized } msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) @@ -143,7 +143,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.node.GetIdentity() if identity == nil { - return ErrIdentityNotInitialized + return ErrorIdentityNotInitialized } if minerType == "" { @@ -182,7 +182,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.node.GetIdentity() if identity == nil { - return ErrIdentityNotInitialized + return ErrorIdentityNotInitialized } payload := StopMinerPayload{ @@ -215,7 +215,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { identity := c.node.GetIdentity() if identity == nil { - return nil, ErrIdentityNotInitialized + return nil, ErrorIdentityNotInitialized } payload := GetLogsPayload{ @@ -274,7 +274,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.node.GetIdentity() if identity == nil { - return 0, ErrIdentityNotInitialized + return 0, ErrorIdentityNotInitialized } sentAt := time.Now() diff --git a/node/dispatcher.go b/node/dispatcher.go index 276be96..769217e 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -100,16 +100,16 @@ func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { // and then to the appropriate intent handler. // // Behaviour: -// - Returns ErrThreatScoreExceeded if the packet's ThreatScore exceeds the +// - Returns ErrorThreatScoreExceeded if the packet's ThreatScore exceeds the // threshold (packet is dropped and logged). -// - Returns ErrUnknownIntent if no handler is registered for the IntentID +// - Returns ErrorUnknownIntent if no handler is registered for the IntentID // (packet is dropped and logged). // - Returns nil on successful delivery to a handler, or any error the // handler itself returns. -// - A nil packet returns ErrNilPacket immediately. +// - A nil packet returns ErrorNilPacket immediately. func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { if pkt == nil { - return ErrNilPacket + return ErrorNilPacket } // 1. Threat circuit breaker (L5 guard) @@ -120,7 +120,7 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), "version": pkt.Header.Version, }) - return ErrThreatScoreExceeded + return ErrorThreatScoreExceeded } // 2. Intent routing (L9 semantic) @@ -133,7 +133,7 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), "version": pkt.Header.Version, }) - return ErrUnknownIntent + return ErrorUnknownIntent } return handler(pkt) @@ -141,14 +141,23 @@ func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { // Sentinel errors returned by Dispatch. var ( - // ErrThreatScoreExceeded is returned when a packet's ThreatScore exceeds + // ErrorThreatScoreExceeded is returned when a packet's ThreatScore exceeds // the safety threshold. - ErrThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) + ErrorThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) - // ErrUnknownIntent is returned when no handler is registered for the + // Deprecated: use ErrorThreatScoreExceeded. + ErrThreatScoreExceeded = ErrorThreatScoreExceeded + + // ErrorUnknownIntent is returned when no handler is registered for the // packet's IntentID. - ErrUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) + ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - // ErrNilPacket is returned when a nil packet is passed to Dispatch. - ErrNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) + // Deprecated: use ErrorUnknownIntent. + ErrUnknownIntent = ErrorUnknownIntent + + // ErrorNilPacket is returned when a nil packet is passed to Dispatch. + ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) + + // Deprecated: use ErrorNilPacket. + ErrNilPacket = ErrorNilPacket ) diff --git a/node/errors.go b/node/errors.go index 96cf152..1ee3f82 100644 --- a/node/errors.go +++ b/node/errors.go @@ -2,13 +2,19 @@ package node import core "dappco.re/go/core" -// Sentinel errors shared across the node package. +// Shared error sentinels for the node package. var ( - // ErrIdentityNotInitialized is returned when a node operation requires + // ErrorIdentityNotInitialized is returned when a node operation requires // a node identity but none has been generated or loaded. - ErrIdentityNotInitialized = core.E("node", "node identity not initialized", nil) + ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // ErrMinerManagerNotConfigured is returned when a miner operation is + // Deprecated: use ErrorIdentityNotInitialized. + ErrIdentityNotInitialized = ErrorIdentityNotInitialized + + // ErrorMinerManagerNotConfigured is returned when a miner operation is // attempted but no MinerManager has been set on the Worker. - ErrMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) + ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) + + // Deprecated: use ErrorMinerManagerNotConfigured. + ErrMinerManagerNotConfigured = ErrorMinerManagerNotConfigured ) diff --git a/node/identity.go b/node/identity.go index a6cb702..636dc82 100644 --- a/node/identity.go +++ b/node/identity.go @@ -191,7 +191,7 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error defer n.mu.RUnlock() if n.privateKey == nil { - return nil, ErrIdentityNotInitialized + return nil, ErrorIdentityNotInitialized } // Load peer's public key diff --git a/node/levin/connection.go b/node/levin/connection.go index 20d7c7d..63a748e 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -131,7 +131,7 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { // Check against the connection-specific payload limit. if h.PayloadSize > c.MaxPayloadSize { - return Header{}, nil, ErrPayloadTooBig + return Header{}, nil, ErrorPayloadTooBig } // Empty payload is valid — return nil data without allocation. diff --git a/node/levin/header.go b/node/levin/header.go index bd7602f..b8b465a 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -22,10 +22,19 @@ const MaxPayloadSize uint64 = 100 * 1024 * 1024 // Return-code constants carried in every Levin response. const ( - ReturnOK int32 = 0 - ReturnErrConnection int32 = -1 - ReturnErrFormat int32 = -7 - ReturnErrSignature int32 = -13 + ReturnOK int32 = 0 + ReturnErrorConnection int32 = -1 + ReturnErrorFormat int32 = -7 + ReturnErrorSignature int32 = -13 + + // Deprecated: use ReturnErrorConnection. + ReturnErrConnection = ReturnErrorConnection + + // Deprecated: use ReturnErrorFormat. + ReturnErrFormat = ReturnErrorFormat + + // Deprecated: use ReturnErrorSignature. + ReturnErrSignature = ReturnErrorSignature ) // Command IDs for the CryptoNote P2P layer. @@ -43,8 +52,14 @@ const ( // Sentinel errors returned by DecodeHeader. var ( - ErrBadSignature = core.E("levin", "bad signature", nil) - ErrPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) + ErrorBadSignature = core.E("levin", "bad signature", nil) + ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) + + // Deprecated: use ErrorBadSignature. + ErrBadSignature = ErrorBadSignature + + // Deprecated: use ErrorPayloadTooBig. + ErrPayloadTooBig = ErrorPayloadTooBig ) // Header is the 33-byte packed header that prefixes every Levin message. @@ -87,11 +102,11 @@ func DecodeHeader(buf [HeaderSize]byte) (Header, error) { var h Header h.Signature = binary.LittleEndian.Uint64(buf[0:8]) if h.Signature != Signature { - return Header{}, ErrBadSignature + return Header{}, ErrorBadSignature } h.PayloadSize = binary.LittleEndian.Uint64(buf[8:16]) if h.PayloadSize > MaxPayloadSize { - return Header{}, ErrPayloadTooBig + return Header{}, ErrorPayloadTooBig } h.ExpectResponse = buf[16] == 0x01 h.Command = binary.LittleEndian.Uint32(buf[17:21]) diff --git a/node/levin/storage.go b/node/levin/storage.go index 85b1428..a03d303 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -40,12 +40,30 @@ const ( // Sentinel errors for storage encoding and decoding. var ( - ErrStorageBadSignature = core.E("levin.storage", "bad storage signature", nil) - ErrStorageTruncated = core.E("levin.storage", "truncated storage data", nil) - ErrStorageBadVersion = core.E("levin.storage", "unsupported storage version", nil) - ErrStorageNameTooLong = core.E("levin.storage", "entry name exceeds 255 bytes", nil) - ErrStorageTypeMismatch = core.E("levin.storage", "value type mismatch", nil) - ErrStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) + ErrorStorageBadSignature = core.E("levin.storage", "bad storage signature", nil) + ErrorStorageTruncated = core.E("levin.storage", "truncated storage data", nil) + ErrorStorageBadVersion = core.E("levin.storage", "unsupported storage version", nil) + ErrorStorageNameTooLong = core.E("levin.storage", "entry name exceeds 255 bytes", nil) + ErrorStorageTypeMismatch = core.E("levin.storage", "value type mismatch", nil) + ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) + + // Deprecated: use ErrorStorageBadSignature. + ErrStorageBadSignature = ErrorStorageBadSignature + + // Deprecated: use ErrorStorageTruncated. + ErrStorageTruncated = ErrorStorageTruncated + + // Deprecated: use ErrorStorageBadVersion. + ErrStorageBadVersion = ErrorStorageBadVersion + + // Deprecated: use ErrorStorageNameTooLong. + ErrStorageNameTooLong = ErrorStorageNameTooLong + + // Deprecated: use ErrorStorageTypeMismatch. + ErrStorageTypeMismatch = ErrorStorageTypeMismatch + + // Deprecated: use ErrorStorageUnknownType. + ErrStorageUnknownType = ErrorStorageUnknownType ) // Section is an ordered map of named values forming a portable storage section. @@ -179,7 +197,7 @@ func ObjectArrayVal(vs []Section) Value { // AsUint64 returns the uint64 value or an error on type mismatch. func (v Value) AsUint64() (uint64, error) { if v.Type != TypeUint64 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return v.uintVal, nil } @@ -187,7 +205,7 @@ func (v Value) AsUint64() (uint64, error) { // AsUint32 returns the uint32 value or an error on type mismatch. func (v Value) AsUint32() (uint32, error) { if v.Type != TypeUint32 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return uint32(v.uintVal), nil } @@ -195,7 +213,7 @@ func (v Value) AsUint32() (uint32, error) { // AsUint16 returns the uint16 value or an error on type mismatch. func (v Value) AsUint16() (uint16, error) { if v.Type != TypeUint16 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return uint16(v.uintVal), nil } @@ -203,7 +221,7 @@ func (v Value) AsUint16() (uint16, error) { // AsUint8 returns the uint8 value or an error on type mismatch. func (v Value) AsUint8() (uint8, error) { if v.Type != TypeUint8 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return uint8(v.uintVal), nil } @@ -211,7 +229,7 @@ func (v Value) AsUint8() (uint8, error) { // AsInt64 returns the int64 value or an error on type mismatch. func (v Value) AsInt64() (int64, error) { if v.Type != TypeInt64 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return v.intVal, nil } @@ -219,7 +237,7 @@ func (v Value) AsInt64() (int64, error) { // AsInt32 returns the int32 value or an error on type mismatch. func (v Value) AsInt32() (int32, error) { if v.Type != TypeInt32 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return int32(v.intVal), nil } @@ -227,7 +245,7 @@ func (v Value) AsInt32() (int32, error) { // AsInt16 returns the int16 value or an error on type mismatch. func (v Value) AsInt16() (int16, error) { if v.Type != TypeInt16 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return int16(v.intVal), nil } @@ -235,7 +253,7 @@ func (v Value) AsInt16() (int16, error) { // AsInt8 returns the int8 value or an error on type mismatch. func (v Value) AsInt8() (int8, error) { if v.Type != TypeInt8 { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return int8(v.intVal), nil } @@ -243,7 +261,7 @@ func (v Value) AsInt8() (int8, error) { // AsBool returns the bool value or an error on type mismatch. func (v Value) AsBool() (bool, error) { if v.Type != TypeBool { - return false, ErrStorageTypeMismatch + return false, ErrorStorageTypeMismatch } return v.boolVal, nil } @@ -251,7 +269,7 @@ func (v Value) AsBool() (bool, error) { // AsDouble returns the float64 value or an error on type mismatch. func (v Value) AsDouble() (float64, error) { if v.Type != TypeDouble { - return 0, ErrStorageTypeMismatch + return 0, ErrorStorageTypeMismatch } return v.floatVal, nil } @@ -259,7 +277,7 @@ func (v Value) AsDouble() (float64, error) { // AsString returns the byte-string value or an error on type mismatch. func (v Value) AsString() ([]byte, error) { if v.Type != TypeString { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.bytesVal, nil } @@ -267,7 +285,7 @@ func (v Value) AsString() ([]byte, error) { // AsSection returns the nested Section or an error on type mismatch. func (v Value) AsSection() (Section, error) { if v.Type != TypeObject { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.objectVal, nil } @@ -279,7 +297,7 @@ func (v Value) AsSection() (Section, error) { // AsUint64Array returns the []uint64 array or an error on type mismatch. func (v Value) AsUint64Array() ([]uint64, error) { if v.Type != (ArrayFlag | TypeUint64) { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.uint64Array, nil } @@ -287,7 +305,7 @@ func (v Value) AsUint64Array() ([]uint64, error) { // AsUint32Array returns the []uint32 array or an error on type mismatch. func (v Value) AsUint32Array() ([]uint32, error) { if v.Type != (ArrayFlag | TypeUint32) { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.uint32Array, nil } @@ -295,7 +313,7 @@ func (v Value) AsUint32Array() ([]uint32, error) { // AsStringArray returns the [][]byte array or an error on type mismatch. func (v Value) AsStringArray() ([][]byte, error) { if v.Type != (ArrayFlag | TypeString) { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.stringArray, nil } @@ -303,7 +321,7 @@ func (v Value) AsStringArray() ([][]byte, error) { // AsSectionArray returns the []Section array or an error on type mismatch. func (v Value) AsSectionArray() ([]Section, error) { if v.Type != (ArrayFlag | TypeObject) { - return nil, ErrStorageTypeMismatch + return nil, ErrorStorageTypeMismatch } return v.objectArray, nil } @@ -348,7 +366,7 @@ func encodeSection(buf []byte, s Section) ([]byte, error) { // Name: uint8 length + raw bytes. if len(name) > 255 { - return nil, ErrStorageNameTooLong + return nil, ErrorStorageNameTooLong } buf = append(buf, byte(len(name))) buf = append(buf, name...) @@ -431,7 +449,7 @@ func encodeValue(buf []byte, v Value) ([]byte, error) { return encodeSection(buf, v.objectVal) default: - return nil, core.E("levin.encodeValue", core.Sprintf("unknown type tag: 0x%02x", v.Type), ErrStorageUnknownType) + return nil, core.E("levin.encodeValue", core.Sprintf("unknown type tag: 0x%02x", v.Type), ErrorStorageUnknownType) } } @@ -478,7 +496,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { return buf, nil default: - return nil, core.E("levin.encodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return nil, core.E("levin.encodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrorStorageUnknownType) } } @@ -492,7 +510,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { // section, err := DecodeStorage(data) func DecodeStorage(data []byte) (Section, error) { if len(data) < StorageHeaderSize { - return nil, ErrStorageTruncated + return nil, ErrorStorageTruncated } sigA := binary.LittleEndian.Uint32(data[0:4]) @@ -500,10 +518,10 @@ func DecodeStorage(data []byte) (Section, error) { ver := data[8] if sigA != StorageSignatureA || sigB != StorageSignatureB { - return nil, ErrStorageBadSignature + return nil, ErrorStorageBadSignature } if ver != StorageVersion { - return nil, ErrStorageBadVersion + return nil, ErrorStorageBadVersion } s, _, err := decodeSection(data[StorageHeaderSize:]) @@ -524,21 +542,21 @@ func decodeSection(buf []byte) (Section, int, error) { for range count { // Name length (1 byte). if off >= len(buf) { - return nil, 0, ErrStorageTruncated + return nil, 0, ErrorStorageTruncated } nameLen := int(buf[off]) off++ // Name bytes. if off+nameLen > len(buf) { - return nil, 0, ErrStorageTruncated + return nil, 0, ErrorStorageTruncated } name := string(buf[off : off+nameLen]) off += nameLen // Type tag (1 byte). if off >= len(buf) { - return nil, 0, ErrStorageTruncated + return nil, 0, ErrorStorageTruncated } tag := buf[off] off++ @@ -567,68 +585,68 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { switch tag { case TypeUint64: if len(buf) < 8 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := binary.LittleEndian.Uint64(buf[:8]) return Value{Type: TypeUint64, uintVal: v}, 8, nil case TypeInt64: if len(buf) < 8 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := int64(binary.LittleEndian.Uint64(buf[:8])) return Value{Type: TypeInt64, intVal: v}, 8, nil case TypeDouble: if len(buf) < 8 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } bits := binary.LittleEndian.Uint64(buf[:8]) return Value{Type: TypeDouble, floatVal: math.Float64frombits(bits)}, 8, nil case TypeUint32: if len(buf) < 4 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := binary.LittleEndian.Uint32(buf[:4]) return Value{Type: TypeUint32, uintVal: uint64(v)}, 4, nil case TypeInt32: if len(buf) < 4 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := int32(binary.LittleEndian.Uint32(buf[:4])) return Value{Type: TypeInt32, intVal: int64(v)}, 4, nil case TypeUint16: if len(buf) < 2 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := binary.LittleEndian.Uint16(buf[:2]) return Value{Type: TypeUint16, uintVal: uint64(v)}, 2, nil case TypeInt16: if len(buf) < 2 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } v := int16(binary.LittleEndian.Uint16(buf[:2])) return Value{Type: TypeInt16, intVal: int64(v)}, 2, nil case TypeUint8: if len(buf) < 1 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } return Value{Type: TypeUint8, uintVal: uint64(buf[0])}, 1, nil case TypeInt8: if len(buf) < 1 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } return Value{Type: TypeInt8, intVal: int64(int8(buf[0]))}, 1, nil case TypeBool: if len(buf) < 1 { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } return Value{Type: TypeBool, boolVal: buf[0] != 0}, 1, nil @@ -638,7 +656,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { return Value{}, 0, err } if uint64(len(buf)-n) < strLen { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } data := make([]byte, strLen) copy(data, buf[n:n+int(strLen)]) @@ -652,7 +670,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { return Value{Type: TypeObject, objectVal: sec}, consumed, nil default: - return Value{}, 0, core.E("levin.decodeValue", core.Sprintf("unknown type tag: 0x%02x", tag), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeValue", core.Sprintf("unknown type tag: 0x%02x", tag), ErrorStorageUnknownType) } } @@ -671,7 +689,7 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { arr := make([]uint64, count) for i := range count { if off+8 > len(buf) { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } arr[i] = binary.LittleEndian.Uint64(buf[off : off+8]) off += 8 @@ -682,7 +700,7 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { arr := make([]uint32, count) for i := range count { if off+4 > len(buf) { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } arr[i] = binary.LittleEndian.Uint32(buf[off : off+4]) off += 4 @@ -698,7 +716,7 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { } off += sn if uint64(len(buf)-off) < strLen { - return Value{}, 0, ErrStorageTruncated + return Value{}, 0, ErrorStorageTruncated } data := make([]byte, strLen) copy(data, buf[off:off+int(strLen)]) @@ -720,6 +738,6 @@ func decodeArray(buf []byte, tag uint8) (Value, int, error) { return Value{Type: tag, objectArray: arr}, off, nil default: - return Value{}, 0, core.E("levin.decodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrStorageUnknownType) + return Value{}, 0, core.E("levin.decodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrorStorageUnknownType) } } diff --git a/node/levin/varint.go b/node/levin/varint.go index 97d3e93..740a0df 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -22,11 +22,17 @@ const ( varintMax8 = 4_611_686_018_427_387_903 ) -// ErrVarintTruncated is returned when the buffer is too short. -var ErrVarintTruncated = core.E("levin", "truncated varint", nil) +// ErrorVarintTruncated is returned when the buffer is too short. +var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) -// ErrVarintOverflow is returned when the value is too large to encode. -var ErrVarintOverflow = core.E("levin", "varint overflow", nil) +// Deprecated: use ErrorVarintTruncated. +var ErrVarintTruncated = ErrorVarintTruncated + +// ErrorVarintOverflow is returned when the value is too large to encode. +var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) + +// Deprecated: use ErrorVarintOverflow. +var ErrVarintOverflow = ErrorVarintOverflow // PackVarint encodes v using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; @@ -61,7 +67,7 @@ func PackVarint(v uint64) []byte { // value, err := UnpackVarint(data) func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { if len(buf) == 0 { - return 0, 0, ErrVarintTruncated + return 0, 0, ErrorVarintTruncated } mark := buf[0] & varintMask @@ -72,27 +78,27 @@ func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { return value, 1, nil case varintMark2: if len(buf) < 2 { - return 0, 0, ErrVarintTruncated + return 0, 0, ErrorVarintTruncated } raw := binary.LittleEndian.Uint16(buf[:2]) value = uint64(raw) >> 2 return value, 2, nil case varintMark4: if len(buf) < 4 { - return 0, 0, ErrVarintTruncated + return 0, 0, ErrorVarintTruncated } raw := binary.LittleEndian.Uint32(buf[:4]) value = uint64(raw) >> 2 return value, 4, nil case varintMark8: if len(buf) < 8 { - return 0, 0, ErrVarintTruncated + return 0, 0, ErrorVarintTruncated } raw := binary.LittleEndian.Uint64(buf[:8]) value = raw >> 2 return value, 8, nil default: // Unreachable — mark is masked to 2 bits. - return 0, 0, ErrVarintTruncated + return 0, 0, ErrorVarintTruncated } } diff --git a/node/message.go b/node/message.go index a10aa96..5b30a8a 100644 --- a/node/message.go +++ b/node/message.go @@ -8,11 +8,11 @@ import ( "github.com/google/uuid" ) -// Protocol version constants +// Protocol version constants. const ( - // ProtocolVersion is the current protocol version + // ProtocolVersion is the current protocol version. ProtocolVersion = "1.0" - // MinProtocolVersion is the minimum supported version + // MinProtocolVersion is the minimum supported version. MinProtocolVersion = "1.0" ) @@ -273,26 +273,44 @@ type DeployAckPayload struct { // ErrorPayload contains error information. // -// payload := ErrorPayload{Code: ErrCodeOperationFailed, Message: "start failed"} +// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} type ErrorPayload struct { Code int `json:"code"` Message string `json:"message"` Details string `json:"details,omitempty"` } -// Common error codes +// Common error codes. const ( - ErrCodeUnknown = 1000 - ErrCodeInvalidMessage = 1001 - ErrCodeUnauthorized = 1002 - ErrCodeNotFound = 1003 - ErrCodeOperationFailed = 1004 - ErrCodeTimeout = 1005 + ErrorCodeUnknown = 1000 + ErrorCodeInvalidMessage = 1001 + ErrorCodeUnauthorized = 1002 + ErrorCodeNotFound = 1003 + ErrorCodeOperationFailed = 1004 + ErrorCodeTimeout = 1005 + + // Deprecated: use ErrorCodeUnknown. + ErrCodeUnknown = ErrorCodeUnknown + + // Deprecated: use ErrorCodeInvalidMessage. + ErrCodeInvalidMessage = ErrorCodeInvalidMessage + + // Deprecated: use ErrorCodeUnauthorized. + ErrCodeUnauthorized = ErrorCodeUnauthorized + + // Deprecated: use ErrorCodeNotFound. + ErrCodeNotFound = ErrorCodeNotFound + + // Deprecated: use ErrorCodeOperationFailed. + ErrCodeOperationFailed = ErrorCodeOperationFailed + + // Deprecated: use ErrorCodeTimeout. + ErrCodeTimeout = ErrorCodeTimeout ) // NewErrorMessage creates an error response message. // -// msg, err := NewErrorMessage("worker", "controller", ErrCodeOperationFailed, "miner start failed", "req-1") +// msg, err := NewErrorMessage("worker", "controller", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { msg, err := NewMessage(MsgError, from, to, ErrorPayload{ Code: code, diff --git a/node/protocol.go b/node/protocol.go index 36c51c6..790fb07 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -6,7 +6,7 @@ import ( // ProtocolError represents an error from the remote peer. // -// err := &ProtocolError{Code: ErrCodeOperationFailed, Message: "start failed"} +// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} type ProtocolError struct { Code int Message string @@ -35,7 +35,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy if resp.Type == MsgError { var errPayload ErrorPayload if err := resp.ParsePayload(&errPayload); err != nil { - return &ProtocolError{Code: ErrCodeUnknown, Message: "unable to parse error response"} + return &ProtocolError{Code: ErrorCodeUnknown, Message: "unable to parse error response"} } return &ProtocolError{Code: errPayload.Code, Message: errPayload.Message} } diff --git a/node/transport.go b/node/transport.go index fe6e282..e7bd859 100644 --- a/node/transport.go +++ b/node/transport.go @@ -641,7 +641,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { identity := t.node.GetIdentity() if identity == nil { - return ErrIdentityNotInitialized + return ErrorIdentityNotInitialized } // Generate challenge for the server to prove it has the matching private key diff --git a/node/worker.go b/node/worker.go index 23d3178..9b32b36 100644 --- a/node/worker.go +++ b/node/worker.go @@ -103,7 +103,7 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { errMsg, _ := NewErrorMessage( identity.ID, msg.From, - ErrCodeOperationFailed, + ErrorCodeOperationFailed, err.Error(), msg.ID, ) @@ -141,7 +141,7 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { func (w *Worker) handleGetStats(msg *Message) (*Message, error) { identity := w.node.GetIdentity() if identity == nil { - return nil, ErrIdentityNotInitialized + return nil, ErrorIdentityNotInitialized } stats := StatsPayload{ @@ -204,7 +204,7 @@ func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { // handleStartMiner starts a miner with the given profile. func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { if w.minerManager == nil { - return nil, ErrMinerManagerNotConfigured + return nil, ErrorMinerManagerNotConfigured } var payload StartMinerPayload @@ -251,7 +251,7 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { // handleStopMiner stops a running miner. func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { if w.minerManager == nil { - return nil, ErrMinerManagerNotConfigured + return nil, ErrorMinerManagerNotConfigured } var payload StopMinerPayload @@ -274,7 +274,7 @@ func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { // handleGetLogs returns console logs from a miner. func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { if w.minerManager == nil { - return nil, ErrMinerManagerNotConfigured + return nil, ErrorMinerManagerNotConfigured } var payload GetLogsPayload diff --git a/specs/node-levin.md b/specs/node-levin.md index b5e8912..2c60cb1 100644 --- a/specs/node-levin.md +++ b/specs/node-levin.md @@ -99,19 +99,19 @@ Tagged portable-storage value. The exported `Type` field identifies which intern | Name | Signature | Description | | --- | --- | --- | -| `AsUint64` | `func (v Value) AsUint64() (uint64, error)` | Returns the scalar `uint64` value or `ErrStorageTypeMismatch`. | -| `AsUint32` | `func (v Value) AsUint32() (uint32, error)` | Returns the scalar `uint32` value or `ErrStorageTypeMismatch`. | -| `AsUint16` | `func (v Value) AsUint16() (uint16, error)` | Returns the scalar `uint16` value or `ErrStorageTypeMismatch`. | -| `AsUint8` | `func (v Value) AsUint8() (uint8, error)` | Returns the scalar `uint8` value or `ErrStorageTypeMismatch`. | -| `AsInt64` | `func (v Value) AsInt64() (int64, error)` | Returns the scalar `int64` value or `ErrStorageTypeMismatch`. | -| `AsInt32` | `func (v Value) AsInt32() (int32, error)` | Returns the scalar `int32` value or `ErrStorageTypeMismatch`. | -| `AsInt16` | `func (v Value) AsInt16() (int16, error)` | Returns the scalar `int16` value or `ErrStorageTypeMismatch`. | -| `AsInt8` | `func (v Value) AsInt8() (int8, error)` | Returns the scalar `int8` value or `ErrStorageTypeMismatch`. | -| `AsBool` | `func (v Value) AsBool() (bool, error)` | Returns the scalar `bool` value or `ErrStorageTypeMismatch`. | -| `AsDouble` | `func (v Value) AsDouble() (float64, error)` | Returns the scalar `float64` value or `ErrStorageTypeMismatch`. | -| `AsString` | `func (v Value) AsString() ([]byte, error)` | Returns the scalar byte-string or `ErrStorageTypeMismatch`. | -| `AsSection` | `func (v Value) AsSection() (Section, error)` | Returns the nested `Section` or `ErrStorageTypeMismatch`. | -| `AsUint64Array` | `func (v Value) AsUint64Array() ([]uint64, error)` | Returns the `[]uint64` array or `ErrStorageTypeMismatch`. | -| `AsUint32Array` | `func (v Value) AsUint32Array() ([]uint32, error)` | Returns the `[]uint32` array or `ErrStorageTypeMismatch`. | -| `AsStringArray` | `func (v Value) AsStringArray() ([][]byte, error)` | Returns the `[][]byte` array or `ErrStorageTypeMismatch`. | -| `AsSectionArray` | `func (v Value) AsSectionArray() ([]Section, error)` | Returns the `[]Section` array or `ErrStorageTypeMismatch`. | +| `AsUint64` | `func (v Value) AsUint64() (uint64, error)` | Returns the scalar `uint64` value or `ErrorStorageTypeMismatch`. | +| `AsUint32` | `func (v Value) AsUint32() (uint32, error)` | Returns the scalar `uint32` value or `ErrorStorageTypeMismatch`. | +| `AsUint16` | `func (v Value) AsUint16() (uint16, error)` | Returns the scalar `uint16` value or `ErrorStorageTypeMismatch`. | +| `AsUint8` | `func (v Value) AsUint8() (uint8, error)` | Returns the scalar `uint8` value or `ErrorStorageTypeMismatch`. | +| `AsInt64` | `func (v Value) AsInt64() (int64, error)` | Returns the scalar `int64` value or `ErrorStorageTypeMismatch`. | +| `AsInt32` | `func (v Value) AsInt32() (int32, error)` | Returns the scalar `int32` value or `ErrorStorageTypeMismatch`. | +| `AsInt16` | `func (v Value) AsInt16() (int16, error)` | Returns the scalar `int16` value or `ErrorStorageTypeMismatch`. | +| `AsInt8` | `func (v Value) AsInt8() (int8, error)` | Returns the scalar `int8` value or `ErrorStorageTypeMismatch`. | +| `AsBool` | `func (v Value) AsBool() (bool, error)` | Returns the scalar `bool` value or `ErrorStorageTypeMismatch`. | +| `AsDouble` | `func (v Value) AsDouble() (float64, error)` | Returns the scalar `float64` value or `ErrorStorageTypeMismatch`. | +| `AsString` | `func (v Value) AsString() ([]byte, error)` | Returns the scalar byte-string or `ErrorStorageTypeMismatch`. | +| `AsSection` | `func (v Value) AsSection() (Section, error)` | Returns the nested `Section` or `ErrorStorageTypeMismatch`. | +| `AsUint64Array` | `func (v Value) AsUint64Array() ([]uint64, error)` | Returns the `[]uint64` array or `ErrorStorageTypeMismatch`. | +| `AsUint32Array` | `func (v Value) AsUint32Array() ([]uint32, error)` | Returns the `[]uint32` array or `ErrorStorageTypeMismatch`. | +| `AsStringArray` | `func (v Value) AsStringArray() ([][]byte, error)` | Returns the `[][]byte` array or `ErrorStorageTypeMismatch`. | +| `AsSectionArray` | `func (v Value) AsSectionArray() ([]Section, error)` | Returns the `[]Section` array or `ErrorStorageTypeMismatch`. | -- 2.45.3 From 4c8bced1e760ec0498f82b7aebf85943b1a31e24 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:03:06 +0000 Subject: [PATCH 10/60] refactor(node): add AX-native accessors and usage docs Co-Authored-By: Virgil --- logging/logger.go | 54 ++++++++++++++++++----------- logging/logger_test.go | 8 ++--- node/controller.go | 73 +++++++++++++++++++++++++++------------- node/identity.go | 28 ++++++++++----- node/levin/connection.go | 17 +++++++--- node/message.go | 17 ++++++---- node/peer.go | 67 +++++++++++++++++++++++++++--------- node/protocol.go | 22 +++++++----- node/transport.go | 67 ++++++++++++++++++++++-------------- node/worker.go | 16 ++++++--- 10 files changed, 247 insertions(+), 122 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index 739b54d..e7e0858 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -47,7 +47,7 @@ func (l Level) String() string { // // logger := New(DefaultConfig()) type Logger struct { - mu sync.Mutex + mu sync.RWMutex output io.Writer level Level component string @@ -73,7 +73,7 @@ func DefaultConfig() Config { } } -// New creates a new Logger with the given configuration. +// New creates a logger from an explicit configuration. // // logger := New(DefaultConfig()) func New(cfg Config) *Logger { @@ -103,20 +103,29 @@ func (l *Logger) WithComponent(component string) *Logger { return l.ComponentLogger(component) } -// SetLevel sets the minimum log level. +// SetLevel changes the minimum log level. +// +// logger.SetLevel(LevelDebug) func (l *Logger) SetLevel(level Level) { l.mu.Lock() defer l.mu.Unlock() l.level = level } -// GetLevel returns the current log level. -func (l *Logger) GetLevel() Level { - l.mu.Lock() - defer l.mu.Unlock() +// Level returns the current log level. +// +// level := logger.Level() +func (l *Logger) Level() Level { + l.mu.RLock() + defer l.mu.RUnlock() return l.level } +// Deprecated: use Level. +func (l *Logger) GetLevel() Level { + return l.Level() +} + // Fields represents key-value pairs for structured logging. // // fields := Fields{"peer_id": "node-1", "attempt": 2} @@ -234,7 +243,7 @@ var ( globalMu sync.RWMutex ) -// SetGlobal sets the global logger instance. +// SetGlobal installs the global logger instance. // // SetGlobal(New(DefaultConfig())) func SetGlobal(l *Logger) { @@ -243,16 +252,21 @@ func SetGlobal(l *Logger) { globalLogger = l } -// GetGlobal returns the global logger instance. +// Global returns the global logger instance. // -// logger := GetGlobal() -func GetGlobal() *Logger { +// logger := Global() +func Global() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger } -// SetGlobalLevel sets the log level of the global logger. +// Deprecated: use Global. +func GetGlobal() *Logger { + return Global() +} + +// SetGlobalLevel changes the global logger level. // // SetGlobalLevel(LevelDebug) func SetGlobalLevel(level Level) { @@ -267,56 +281,56 @@ func SetGlobalLevel(level Level) { // // Debug("connected", Fields{"peer_id": "node-1"}) func Debug(msg string, fields ...Fields) { - GetGlobal().Debug(msg, fields...) + Global().Debug(msg, fields...) } // Info logs an informational message using the global logger. // // Info("worker started", Fields{"component": "transport"}) func Info(msg string, fields ...Fields) { - GetGlobal().Info(msg, fields...) + Global().Info(msg, fields...) } // Warn logs a warning message using the global logger. // // Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(msg string, fields ...Fields) { - GetGlobal().Warn(msg, fields...) + Global().Warn(msg, fields...) } // Error logs an error message using the global logger. // // Error("send failed", Fields{"peer_id": "node-1"}) func Error(msg string, fields ...Fields) { - GetGlobal().Error(msg, fields...) + Global().Error(msg, fields...) } // Debugf logs a formatted debug message using the global logger. // // Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { - GetGlobal().Debugf(format, args...) + Global().Debugf(format, args...) } // Infof logs a formatted informational message using the global logger. // // Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { - GetGlobal().Infof(format, args...) + Global().Infof(format, args...) } // Warnf logs a formatted warning message using the global logger. // // Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { - GetGlobal().Warnf(format, args...) + Global().Warnf(format, args...) } // Errorf logs a formatted error message using the global logger. // // Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { - GetGlobal().Errorf(format, args...) + Global().Errorf(format, args...) } // ParseLevel parses a string into a log level. diff --git a/logging/logger_test.go b/logging/logger_test.go index 0850cde..11ba5cb 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -100,7 +100,7 @@ func TestLogger_ComponentLogger_Good(t *testing.T) { child := parent.ComponentLogger("ChildComponent") child.Info("child message") - alias := parent.WithComponent("AliasComponent") + alias := parent.ComponentLogger("AliasComponent") alias.Info("alias message") output := buf.String() @@ -147,9 +147,9 @@ func TestLogger_SetLevel_Good(t *testing.T) { t.Error("Info should appear after level change") } - // Verify GetLevel - if logger.GetLevel() != LevelInfo { - t.Error("GetLevel should return LevelInfo") + // Verify Level + if logger.Level() != LevelInfo { + t.Error("Level should return LevelInfo") } } diff --git a/node/controller.go b/node/controller.go index 90e9085..e7e8f39 100644 --- a/node/controller.go +++ b/node/controller.go @@ -23,7 +23,7 @@ type Controller struct { pending map[string]chan *Message // message ID -> response channel } -// NewController creates a new Controller instance. +// NewController wires a controller to a node manager, peer registry, and transport. // // controller := NewController(nodeManager, peerRegistry, transport) func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller { @@ -67,8 +67,8 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat actualPeerID := peerID // Auto-connect if not already connected - if c.transport.GetConnection(peerID) == nil { - peer := c.peers.GetPeer(peerID) + if c.transport.Connection(peerID) == nil { + peer := c.peers.Peer(peerID) if peer == nil { return nil, core.E("Controller.sendRequest", "peer not found: "+peerID, nil) } @@ -114,16 +114,18 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat } } -// GetRemoteStats requests miner statistics from a remote peer. -func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { - identity := c.node.GetIdentity() +// RemoteStats requests miner statistics from a remote peer. +// +// stats, err := controller.RemoteStats("worker-1") +func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { + identity := c.node.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) if err != nil { - return nil, core.E("Controller.GetRemoteStats", "failed to create message", err) + return nil, core.E("Controller.RemoteStats", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -139,9 +141,14 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } +// Deprecated: use RemoteStats. +func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { + return c.RemoteStats(peerID) +} + // StartRemoteMiner requests a remote peer to start a miner with a given profile. func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { - identity := c.node.GetIdentity() + identity := c.node.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -180,7 +187,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi // StopRemoteMiner requests a remote peer to stop a miner. func (c *Controller) StopRemoteMiner(peerID, minerName string) error { - identity := c.node.GetIdentity() + identity := c.node.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -211,9 +218,11 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return nil } -// GetRemoteLogs requests console logs from a remote miner. -func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { - identity := c.node.GetIdentity() +// RemoteLogs requests console logs from a remote miner. +// +// logs, err := controller.RemoteLogs("worker-1", "xmrig-0", 100) +func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, error) { + identity := c.node.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -225,7 +234,7 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin msg, err := NewMessage(MsgGetLogs, identity.ID, peerID, payload) if err != nil { - return nil, core.E("Controller.GetRemoteLogs", "failed to create message", err) + return nil, core.E("Controller.RemoteLogs", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -241,8 +250,15 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// GetAllStats fetches stats from all connected peers. -func (c *Controller) GetAllStats() map[string]*StatsPayload { +// Deprecated: use RemoteLogs. +func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { + return c.RemoteLogs(peerID, minerName, lines) +} + +// AllStats fetches stats from all connected peers. +// +// statsByPeerID := controller.AllStats() +func (c *Controller) AllStats() map[string]*StatsPayload { results := make(map[string]*StatsPayload) var mu sync.Mutex var wg sync.WaitGroup @@ -251,7 +267,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { wg.Add(1) go func(p *Peer) { defer wg.Done() - stats, err := c.GetRemoteStats(p.ID) + stats, err := c.RemoteStats(p.ID) if err != nil { logging.Debug("failed to get stats from peer", logging.Fields{ "peer_id": p.ID, @@ -270,9 +286,16 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// PingPeer sends a ping to a peer and updates metrics. +// Deprecated: use AllStats. +func (c *Controller) GetAllStats() map[string]*StatsPayload { + return c.AllStats() +} + +// PingPeer sends a ping to a peer and refreshes that peer's metrics. +// +// rttMS, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { - identity := c.node.GetIdentity() + identity := c.node.Identity() if identity == nil { return 0, ErrorIdentityNotInitialized } @@ -300,7 +323,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { rtt := time.Since(sentAt).Seconds() * 1000 // Convert to ms // Update peer metrics - peer := c.peers.GetPeer(peerID) + peer := c.peers.Peer(peerID) if peer != nil { c.peers.UpdateMetrics(peerID, rtt, peer.GeoKM, peer.Hops) } @@ -308,9 +331,11 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return rtt, nil } -// ConnectToPeer establishes a connection to a peer. +// ConnectToPeer opens a transport connection to a peer. +// +// err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { - peer := c.peers.GetPeer(peerID) + peer := c.peers.Peer(peerID) if peer == nil { return core.E("Controller.ConnectToPeer", "peer not found: "+peerID, nil) } @@ -319,9 +344,11 @@ func (c *Controller) ConnectToPeer(peerID string) error { return err } -// DisconnectFromPeer closes connection to a peer. +// DisconnectFromPeer closes the active connection to a peer. +// +// err := controller.DisconnectFromPeer("worker-1") func (c *Controller) DisconnectFromPeer(peerID string) error { - conn := c.transport.GetConnection(peerID) + conn := c.transport.Connection(peerID) if conn == nil { return core.E("Controller.DisconnectFromPeer", "peer not connected: "+peerID, nil) } diff --git a/node/identity.go b/node/identity.go index 636dc82..38bf5e1 100644 --- a/node/identity.go +++ b/node/identity.go @@ -85,7 +85,7 @@ type NodeManager struct { mu sync.RWMutex } -// NewNodeManager creates a new NodeManager, loading existing identity if available. +// NewNodeManager loads the default node identity store. // // nodeManager, err := NewNodeManager() func NewNodeManager() (*NodeManager, error) { @@ -102,7 +102,7 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// NewNodeManagerFromPaths creates a NodeManager using explicit key and config paths. +// NewNodeManagerFromPaths loads or creates a node identity store at explicit paths. // // nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { @@ -132,8 +132,10 @@ func (n *NodeManager) HasIdentity() bool { return n.identity != nil } -// GetIdentity returns the node's public identity. -func (n *NodeManager) GetIdentity() *NodeIdentity { +// Identity returns a copy of the loaded node identity. +// +// identity := nodeManager.Identity() +func (n *NodeManager) Identity() *NodeIdentity { n.mu.RLock() defer n.mu.RUnlock() if n.identity == nil { @@ -144,7 +146,14 @@ func (n *NodeManager) GetIdentity() *NodeIdentity { return &identity } -// GenerateIdentity creates a new node identity with the given name and role. +// Deprecated: use Identity. +func (n *NodeManager) GetIdentity() *NodeIdentity { + return n.Identity() +} + +// GenerateIdentity writes a new node identity for the given name and role. +// +// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { n.mu.Lock() defer n.mu.Unlock() @@ -184,8 +193,9 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { return nil } -// DeriveSharedSecret derives a shared secret with a peer using X25519 ECDH. -// The result is hashed with SHA-256 for use as a symmetric key. +// DeriveSharedSecret hashes an X25519 shared secret for use as a symmetric key. +// +// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error) { n.mu.RLock() defer n.mu.RUnlock() @@ -288,7 +298,9 @@ func (n *NodeManager) loadIdentity() error { return nil } -// Delete removes the node identity and keys from disk. +// Delete removes the node identity and key files from disk. +// +// err := nodeManager.Delete() func (n *NodeManager) Delete() error { n.mu.Lock() defer n.mu.Unlock() diff --git a/node/levin/connection.go b/node/levin/connection.go index 63a748e..9745f83 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -57,8 +57,9 @@ func NewConnection(conn net.Conn) *Connection { } } -// WritePacket sends a Levin request or notification. It builds a 33-byte -// header, then writes header + payload atomically under the write mutex. +// WritePacket sends a Levin request or notification. +// +// err := conn.WritePacket(CommandPing, payload, true) func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { h := Header{ Signature: Signature, @@ -73,6 +74,8 @@ func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool } // WriteResponse sends a Levin response packet with the given return code. +// +// err := conn.WriteResponse(CommandPing, payload, ReturnOK) func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { h := Header{ Signature: Signature, @@ -110,9 +113,9 @@ func (c *Connection) writeFrame(h *Header, payload []byte) error { return nil } -// ReadPacket reads exactly 33 header bytes, validates the signature, -// checks the payload size against MaxPayloadSize, then reads exactly -// PayloadSize bytes of payload data. +// ReadPacket reads and validates the next Levin packet. +// +// header, payload, err := conn.ReadPacket() func (c *Connection) ReadPacket() (Header, []byte, error) { if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return Header{}, nil, err @@ -148,11 +151,15 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } // Close closes the underlying network connection. +// +// err := conn.Close() func (c *Connection) Close() error { return c.conn.Close() } // RemoteAddr returns the remote address of the underlying connection as a string. +// +// addr := conn.RemoteAddr() func (c *Connection) RemoteAddr() string { return c.conn.RemoteAddr().String() } diff --git a/node/message.go b/node/message.go index 5b30a8a..5617886 100644 --- a/node/message.go +++ b/node/message.go @@ -103,9 +103,9 @@ type Message struct { ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to } -// NewMessage creates a new message with a generated ID and timestamp. +// NewMessage builds a message with a generated ID and timestamp. // -// msg, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: 42}) +// msg, err := NewMessage(MsgPing, "controller", "worker-1", PingPayload{SentAt: 42}) func NewMessage(msgType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -126,7 +126,9 @@ func NewMessage(msgType MessageType, from, to string, payload any) (*Message, er }, nil } -// Reply creates a reply message to this message. +// Reply creates a response message that points back to the original. +// +// reply, err := msg.Reply(MsgPong, PongPayload{SentAt: 42, ReceivedAt: 43}) func (m *Message) Reply(msgType MessageType, payload any) (*Message, error) { reply, err := NewMessage(msgType, m.To, m.From, payload) if err != nil { @@ -136,7 +138,10 @@ func (m *Message) Reply(msgType MessageType, payload any) (*Message, error) { return reply, nil } -// ParsePayload unmarshals the payload into the given struct. +// ParsePayload decodes the payload into the supplied target. +// +// var ping PingPayload +// err := msg.ParsePayload(&ping) func (m *Message) ParsePayload(v any) error { if m.Payload == nil { return nil @@ -308,9 +313,9 @@ const ( ErrCodeTimeout = ErrorCodeTimeout ) -// NewErrorMessage creates an error response message. +// NewErrorMessage builds an error response message for an existing request. // -// msg, err := NewErrorMessage("worker", "controller", ErrorCodeOperationFailed, "miner start failed", "req-1") +// msg, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { msg, err := NewMessage(MsgError, from, to, ErrorPayload{ Code: code, diff --git a/node/peer.go b/node/peer.go index ea96695..d344c39 100644 --- a/node/peer.go +++ b/node/peer.go @@ -122,7 +122,7 @@ var ( scoreWeight = 1.2 ) -// NewPeerRegistry creates a new PeerRegistry, loading existing peers if available. +// NewPeerRegistry loads the default peer registry. // // peerRegistry, err := NewPeerRegistry() func NewPeerRegistry() (*PeerRegistry, error) { @@ -134,7 +134,7 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// NewPeerRegistryFromPath creates a PeerRegistry using an explicit peers file path. +// NewPeerRegistryFromPath loads or creates a peer registry at an explicit path. // // peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { @@ -162,7 +162,9 @@ func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// SetAuthMode sets the authentication mode for peer connections. +// SetAuthMode changes how unknown peers are handled. +// +// registry.SetAuthMode(PeerAuthAllowlist) func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -170,13 +172,20 @@ func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { logging.Info("peer auth mode changed", logging.Fields{"mode": mode}) } -// GetAuthMode returns the current authentication mode. -func (r *PeerRegistry) GetAuthMode() PeerAuthMode { +// AuthMode returns the current authentication mode. +// +// mode := registry.AuthMode() +func (r *PeerRegistry) AuthMode() PeerAuthMode { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.authMode } +// Deprecated: use AuthMode. +func (r *PeerRegistry) GetAuthMode() PeerAuthMode { + return r.AuthMode() +} + // AllowPublicKey adds a public key to the allowlist. func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() @@ -318,8 +327,10 @@ func (r *PeerRegistry) RemovePeer(id string) error { return r.save() } -// GetPeer returns a peer by ID. -func (r *PeerRegistry) GetPeer(id string) *Peer { +// Peer returns a copy of the peer with the supplied ID. +// +// peer := registry.Peer("worker-1") +func (r *PeerRegistry) Peer(id string) *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -333,6 +344,11 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return &peerCopy } +// Deprecated: use Peer. +func (r *PeerRegistry) GetPeer(id string) *Peer { + return r.Peer(id) +} + // ListPeers returns all registered peers. func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) @@ -397,8 +413,10 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { return r.save() } -// SetConnected updates a peer's connection state. -func (r *PeerRegistry) SetConnected(id string, connected bool) { +// MarkConnected updates a peer's connection state. +// +// registry.MarkConnected("worker-1", true) +func (r *PeerRegistry) MarkConnected(id string, connected bool) { r.mu.Lock() defer r.mu.Unlock() @@ -410,6 +428,11 @@ func (r *PeerRegistry) SetConnected(id string, connected bool) { } } +// Deprecated: use MarkConnected. +func (r *PeerRegistry) SetConnected(id string, connected bool) { + r.MarkConnected(id, connected) +} + // Score adjustment constants const ( ScoreSuccessIncrement = 1.0 // Increment for successful interaction @@ -477,8 +500,10 @@ func (r *PeerRegistry) RecordTimeout(id string) { }) } -// GetPeersByScore returns peers sorted by score (highest first). -func (r *PeerRegistry) GetPeersByScore() []*Peer { +// PeersSortedByScore returns peers sorted by score, highest first. +// +// peers := registry.PeersSortedByScore() +func (r *PeerRegistry) PeersSortedByScore() []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -498,10 +523,15 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return peers } +// Deprecated: use PeersSortedByScore. +func (r *PeerRegistry) GetPeersByScore() []*Peer { + return r.PeersSortedByScore() +} + // PeersByScore returns an iterator over peers sorted by score (highest first). func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { - peers := r.GetPeersByScore() + peers := r.PeersSortedByScore() for _, p := range peers { if !yield(p) { return @@ -563,11 +593,18 @@ func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { return peers } -// GetConnectedPeers returns all currently connected peers. -func (r *PeerRegistry) GetConnectedPeers() []*Peer { +// ConnectedPeerList returns all currently connected peers as a slice. +// +// connectedPeers := registry.ConnectedPeerList() +func (r *PeerRegistry) ConnectedPeerList() []*Peer { return slices.Collect(r.ConnectedPeers()) } +// Deprecated: use ConnectedPeerList. +func (r *PeerRegistry) GetConnectedPeers() []*Peer { + return r.ConnectedPeerList() +} + // ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { @@ -748,5 +785,3 @@ func (r *PeerRegistry) load() error { return nil } - -// Example usage inside a connection handler diff --git a/node/protocol.go b/node/protocol.go index 790fb07..ed446f0 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -21,11 +21,9 @@ func (e *ProtocolError) Error() string { // handler := &ResponseHandler{} type ResponseHandler struct{} -// ValidateResponse checks if the response is valid and returns a parsed error if it's an error response. -// It checks: -// 1. If response is nil (returns error) -// 2. If response is an error message (returns ProtocolError) -// 3. If response type matches expected (returns error if not) +// ValidateResponse checks a response against the expected type. +// +// err := handler.ValidateResponse(resp, MsgStats) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { return core.E("ResponseHandler.ValidateResponse", "nil response", nil) @@ -49,7 +47,8 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy } // ParseResponse validates the response and parses the payload into the target. -// This combines ValidateResponse and ParsePayload into a single call. +// +// err := handler.ParseResponse(resp, MsgStats, &stats) func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error { if err := h.ValidateResponse(resp, expectedType); err != nil { return err @@ -89,12 +88,17 @@ func IsProtocolError(err error) bool { return ok } -// GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. +// ProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. // -// code := GetProtocolErrorCode(err) -func GetProtocolErrorCode(err error) int { +// code := ProtocolErrorCode(err) +func ProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code } return 0 } + +// Deprecated: use ProtocolErrorCode. +func GetProtocolErrorCode(err error) int { + return ProtocolErrorCode(err) +} diff --git a/node/transport.go b/node/transport.go index e7bd859..2cb5aed 100644 --- a/node/transport.go +++ b/node/transport.go @@ -187,7 +187,7 @@ type PeerConnection struct { rateLimiter *PeerRateLimiter // Per-peer message rate limiting } -// NewTransport creates a new WebSocket transport. +// NewTransport creates a WebSocket transport for a node and peer registry. // // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { @@ -209,11 +209,11 @@ func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportCon return true // No origin header (non-browser client) } // Allow localhost and 127.0.0.1 origins - u, err := url.Parse(origin) + originURL, err := url.Parse(origin) if err != nil { return false } - host := u.Hostname() + host := originURL.Hostname() return host == "localhost" || host == "127.0.0.1" || host == "::1" }, }, @@ -222,7 +222,7 @@ func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportCon } } -// Start begins listening for incoming connections. +// Start opens the WebSocket listener and background maintenance loops. func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.WSPath, t.handleWSUpgrade) @@ -289,7 +289,7 @@ func (t *Transport) Start() error { return nil } -// Stop gracefully shuts down the transport. +// Stop closes active connections and shuts the transport down cleanly. func (t *Transport) Stop() error { t.cancel() @@ -316,28 +316,29 @@ func (t *Transport) Stop() error { return nil } -// OnMessage sets the handler for incoming messages. -// Must be called before Start() to avoid races. +// OnMessage installs the handler for incoming messages before Start. +// +// transport.OnMessage(worker.HandleMessage) func (t *Transport) OnMessage(handler MessageHandler) { t.mu.Lock() defer t.mu.Unlock() t.handler = handler } -// Connect establishes a connection to a peer. +// Connect dials a peer, completes the handshake, and starts the session loops. func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" if t.config.TLSCertPath != "" { scheme = "wss" } - u := url.URL{Scheme: scheme, Host: peer.Address, Path: t.config.WSPath} + peerURL := url.URL{Scheme: scheme, Host: peer.Address, Path: t.config.WSPath} // Dial the peer with timeout to prevent hanging on unresponsive peers dialer := websocket.Dialer{ HandshakeTimeout: 10 * time.Second, } - conn, _, err := dialer.Dial(u.String(), nil) + conn, _, err := dialer.Dial(peerURL.String(), nil) if err != nil { return nil, core.E("Transport.Connect", "failed to connect to peer", err) } @@ -365,7 +366,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) // Update registry - t.registry.SetConnected(pc.Peer.ID, true) + t.registry.MarkConnected(pc.Peer.ID, true) // Start read loop t.wg.Add(1) @@ -380,7 +381,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return pc, nil } -// Send sends a message to a specific peer. +// Send transmits an encrypted message to a connected peer. func (t *Transport) Send(peerID string, msg *Message) error { t.mu.RLock() pc, exists := t.conns[peerID] @@ -407,7 +408,7 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } } -// Broadcast sends a message to all connected peers except the sender. +// Broadcast sends a message to every connected peer except the sender. // The sender is identified by msg.From and excluded to prevent echo. func (t *Transport) Broadcast(msg *Message) error { conns := slices.Collect(t.Connections()) @@ -425,13 +426,20 @@ func (t *Transport) Broadcast(msg *Message) error { return lastErr } -// GetConnection returns an active connection to a peer. -func (t *Transport) GetConnection(peerID string) *PeerConnection { +// Connection returns an active connection to a peer. +// +// connection := transport.Connection("worker-1") +func (t *Transport) Connection(peerID string) *PeerConnection { t.mu.RLock() defer t.mu.RUnlock() return t.conns[peerID] } +// Deprecated: use Connection. +func (t *Transport) GetConnection(peerID string) *PeerConnection { + return t.Connection(peerID) +} + // handleWSUpgrade handles incoming WebSocket connections. func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { // Enforce MaxConns limit (including pending connections during handshake) @@ -498,7 +506,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "supported_versions": SupportedProtocolVersions, "peer_id": payload.Identity.ID, }) - identity := t.node.GetIdentity() + identity := t.node.Identity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -529,7 +537,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "public_key": safeKeyPrefix(payload.Identity.PublicKey), }) // Send rejection before closing - identity := t.node.GetIdentity() + identity := t.node.Identity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -546,7 +554,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Create peer if not exists (only if auth passed) - peer := t.registry.GetPeer(payload.Identity.ID) + peer := t.registry.Peer(payload.Identity.ID) if peer == nil { // Auto-register the peer since they passed allowlist check peer = &Peer{ @@ -574,7 +582,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Send handshake acknowledgment - identity := t.node.GetIdentity() + identity := t.node.Identity() if identity == nil { conn.Close() return @@ -616,7 +624,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { t.mu.Unlock() // Update registry - t.registry.SetConnected(peer.ID, true) + t.registry.MarkConnected(peer.ID, true) // Start read loop t.wg.Add(1) @@ -639,7 +647,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { pc.Conn.SetReadDeadline(time.Time{}) }() - identity := t.node.GetIdentity() + identity := t.node.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -820,7 +828,7 @@ func (t *Transport) keepalive(pc *PeerConnection) { } // Send ping - identity := t.node.GetIdentity() + identity := t.node.Identity() pingMsg, err := NewMessage(MsgPing, identity.ID, pc.Peer.ID, PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -842,7 +850,7 @@ func (t *Transport) removeConnection(pc *PeerConnection) { delete(t.conns, pc.Peer.ID) t.mu.Unlock() - t.registry.SetConnected(pc.Peer.ID, false) + t.registry.MarkConnected(pc.Peer.ID, false) pc.Close() } @@ -901,7 +909,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { // already manages write deadlines under the lock. Setting it here // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { - identity := pc.transport.node.GetIdentity() + identity := pc.transport.node.Identity() if identity != nil { payload := DisconnectPayload{ Reason: reason, @@ -959,9 +967,16 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeers returns the number of connected peers. -func (t *Transport) ConnectedPeers() int { +// ConnectedPeerCount returns the number of connected peers. +// +// count := transport.ConnectedPeerCount() +func (t *Transport) ConnectedPeerCount() int { t.mu.RLock() defer t.mu.RUnlock() return len(t.conns) } + +// Deprecated: use ConnectedPeerCount. +func (t *Transport) ConnectedPeers() int { + return t.ConnectedPeerCount() +} diff --git a/node/worker.go b/node/worker.go index 9b32b36..13a5557 100644 --- a/node/worker.go +++ b/node/worker.go @@ -63,17 +63,23 @@ func NewWorker(node *NodeManager, transport *Transport) *Worker { } } -// SetMinerManager sets the miner manager for handling miner operations. +// SetMinerManager attaches the miner manager used for miner operations. +// +// worker.SetMinerManager(minerManager) func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager } -// SetProfileManager sets the profile manager for handling profile operations. +// SetProfileManager attaches the profile manager used for profile operations. +// +// worker.SetProfileManager(profileManager) func (w *Worker) SetProfileManager(manager ProfileManager) { w.profileManager = manager } -// HandleMessage processes incoming messages and returns a response. +// HandleMessage routes an incoming message to the correct worker handler. +// +// worker.HandleMessage(conn, msg) func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { var response *Message var err error @@ -98,7 +104,7 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { if err != nil { // Send error response - identity := w.node.GetIdentity() + identity := w.node.Identity() if identity != nil { errMsg, _ := NewErrorMessage( identity.ID, @@ -139,7 +145,7 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { // handleGetStats responds with current miner statistics. func (w *Worker) handleGetStats(msg *Message) (*Message, error) { - identity := w.node.GetIdentity() + identity := w.node.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } -- 2.45.3 From 6a70c6f2346c4715e662625d8153e6724c70f192 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 30 Mar 2026 21:17:43 +0100 Subject: [PATCH 11/60] fix(node): surface corrupted persisted state during load Co-Authored-By: Virgil --- node/identity.go | 12 +++++++++--- node/identity_test.go | 16 ++++++++++++++++ node/peer.go | 12 +++++++++--- node/peer_test.go | 16 ++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/node/identity.go b/node/identity.go index 38bf5e1..2f82de3 100644 --- a/node/identity.go +++ b/node/identity.go @@ -104,6 +104,9 @@ func NewNodeManager() (*NodeManager, error) { // NewNodeManagerFromPaths loads or creates a node identity store at explicit paths. // +// Missing files are treated as a fresh install; malformed or partial identity +// state is returned as an error so callers can handle it explicitly. +// // nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ @@ -111,12 +114,15 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { configPath: configPath, } - // Try to load existing identity - if err := nm.loadIdentity(); err != nil { - // Identity doesn't exist yet, that's ok + // Missing files indicate a first run; anything else is a load failure. + if !fsExists(keyPath) && !fsExists(configPath) { return nm, nil } + if err := nm.loadIdentity(); err != nil { + return nil, err + } + return nm, nil } diff --git a/node/identity_test.go b/node/identity_test.go index 87c03b4..9693716 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -170,6 +170,22 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { }) } +func TestIdentity_NodeManagerFromPaths_CorruptIdentity_Bad(t *testing.T) { + tmpDir := t.TempDir() + keyPath, configPath := testNodeManagerPaths(tmpDir) + + testWriteFile(t, configPath, []byte(`{"id":"node-1","name":"broken","publicKey":"not-json"`), 0o600) + + nm, err := NewNodeManagerFromPaths(keyPath, configPath) + if err == nil { + t.Fatal("expected error when loading a corrupted node identity") + } + + if nm != nil { + t.Fatal("expected nil node manager when identity data is corrupted") + } +} + func TestIdentity_NodeRoles_Good(t *testing.T) { tests := []struct { role NodeRole diff --git a/node/peer.go b/node/peer.go index d344c39..60a09c4 100644 --- a/node/peer.go +++ b/node/peer.go @@ -136,6 +136,9 @@ func NewPeerRegistry() (*PeerRegistry, error) { // NewPeerRegistryFromPath loads or creates a peer registry at an explicit path. // +// Missing files are treated as an empty registry; malformed registry files are +// returned as errors so callers can repair the persisted state. +// // peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ @@ -146,13 +149,16 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { allowedPublicKeys: make(map[string]bool), } - // Try to load existing peers - if err := pr.load(); err != nil { - // No existing peers, that's ok + // Missing files indicate a first run; any existing file must parse cleanly. + if !fsExists(peersPath) { pr.rebuildKDTree() return pr, nil } + if err := pr.load(); err != nil { + return nil, err + } + pr.rebuildKDTree() return pr, nil } diff --git a/node/peer_test.go b/node/peer_test.go index ed4bd70..9b0d506 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -27,6 +27,22 @@ func TestPeer_Registry_NewPeerRegistry_Good(t *testing.T) { } } +func TestPeer_Registry_NewPeerRegistryFromPath_CorruptFile_Bad(t *testing.T) { + tmpDir := t.TempDir() + peersPath := testJoinPath(tmpDir, "peers.json") + + testWriteFile(t, peersPath, []byte(`{"id":"peer-1"`), 0o600) + + pr, err := NewPeerRegistryFromPath(peersPath) + if err == nil { + t.Fatal("expected error when loading a corrupted peer registry") + } + + if pr != nil { + t.Fatal("expected nil peer registry when persisted data is corrupted") + } +} + func TestPeer_Registry_AddPeer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() -- 2.45.3 From 15c961a5f46af306761a70871ba6f3dca8af3e21 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:31:23 +0000 Subject: [PATCH 12/60] fix(controller): remove response-channel close race Co-Authored-By: Virgil --- node/controller.go | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/node/controller.go b/node/controller.go index e7e8f39..8fbe40a 100644 --- a/node/controller.go +++ b/node/controller.go @@ -10,7 +10,7 @@ import ( "dappco.re/go/core/p2p/logging" ) -// Controller manages remote peer operations from a controller node. +// Controller drives remote peer operations from a controller node. // // controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { @@ -40,31 +40,36 @@ func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) return c } -// handleResponse processes incoming messages that are responses to our requests. -func (c *Controller) handleResponse(conn *PeerConnection, msg *Message) { +// handleResponse processes incoming replies and routes them to the waiting request. +func (c *Controller) handleResponse(_ *PeerConnection, msg *Message) { if msg.ReplyTo == "" { return // Not a response, let worker handle it } c.mu.Lock() - ch, exists := c.pending[msg.ReplyTo] - if exists { + responseCh, isPending := c.pending[msg.ReplyTo] + if isPending { delete(c.pending, msg.ReplyTo) } c.mu.Unlock() - if exists && ch != nil { + if isPending && responseCh != nil { select { - case ch <- msg: + case responseCh <- msg: default: - // Channel full or closed + // Late duplicate response; drop it. } } } -// sendRequest sends a message and waits for a response. +// sendRequest registers a temporary response channel, sends msg, and waits +// for the matching reply or timeout. +// +// The response channel is intentionally never closed. Removing it from the +// pending map is enough to stop future routing, and it avoids a late-response +// close/send race after the caller has already timed out. func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Duration) (*Message, error) { - actualPeerID := peerID + resolvedPeerID := peerID // Auto-connect if not already connected if c.transport.Connection(peerID) == nil { @@ -77,28 +82,28 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat return nil, core.E("Controller.sendRequest", "failed to connect to peer", err) } // Use the real peer ID after handshake (it may have changed) - actualPeerID = conn.Peer.ID + resolvedPeerID = conn.Peer.ID // Update the message destination - msg.To = actualPeerID + msg.To = resolvedPeerID } // Create response channel - respCh := make(chan *Message, 1) + responseCh := make(chan *Message, 1) c.mu.Lock() - c.pending[msg.ID] = respCh + c.pending[msg.ID] = responseCh c.mu.Unlock() - // Clean up on exit - ensure channel is closed and removed from map + // Clean up on exit. Deleting the pending entry is enough because + // handleResponse only routes through the map. defer func() { c.mu.Lock() delete(c.pending, msg.ID) c.mu.Unlock() - close(respCh) // Close channel to allow garbage collection }() // Send the message - if err := c.transport.Send(actualPeerID, msg); err != nil { + if err := c.transport.Send(resolvedPeerID, msg); err != nil { return nil, core.E("Controller.sendRequest", "failed to send message", err) } @@ -107,7 +112,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat defer cancel() select { - case resp := <-respCh: + case resp := <-responseCh: return resp, nil case <-ctx.Done(): return nil, core.E("Controller.sendRequest", "request timeout", nil) @@ -147,6 +152,8 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { } // StartRemoteMiner requests a remote peer to start a miner with a given profile. +// +// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.node.Identity() if identity == nil { @@ -186,6 +193,8 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi } // StopRemoteMiner requests a remote peer to stop a miner. +// +// err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.node.Identity() if identity == nil { -- 2.45.3 From 0ca20d53eab6e2188a89240a8f36b2940fa88ae3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:35:50 +0000 Subject: [PATCH 13/60] test(node): migrate tests to AX-native constructors Co-Authored-By: Virgil --- node/bench_test.go | 12 +++--- node/controller_test.go | 20 ++++----- node/identity_test.go | 16 +++---- node/integration_test.go | 12 +++--- node/peer_test.go | 18 ++++---- node/transport_test.go | 4 +- node/worker_test.go | 92 ++++++++++++++++++++-------------------- 7 files changed, 87 insertions(+), 87 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index 072404f..91ef804 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -15,7 +15,7 @@ func BenchmarkIdentityGenerate(b *testing.B) { b.ReportAllocs() for b.Loop() { dir := b.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { b.Fatalf("create node manager: %v", err) } @@ -30,10 +30,10 @@ func BenchmarkDeriveSharedSecret(b *testing.B) { dir1 := b.TempDir() dir2 := b.TempDir() - nm1, _ := NewNodeManagerWithPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) + nm1, _ := NewNodeManagerFromPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) nm1.GenerateIdentity("node1", RoleDual) - nm2, _ := NewNodeManagerWithPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) + nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) peerPubKey := nm2.GetIdentity().PublicKey @@ -145,10 +145,10 @@ func BenchmarkSMSGEncryptDecrypt(b *testing.B) { dir1 := b.TempDir() dir2 := b.TempDir() - nm1, _ := NewNodeManagerWithPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) + nm1, _ := NewNodeManagerFromPaths(testJoinPath(dir1, "k"), testJoinPath(dir1, "n")) nm1.GenerateIdentity("node1", RoleDual) - nm2, _ := NewNodeManagerWithPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) + nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) sharedSecret, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) @@ -197,7 +197,7 @@ func BenchmarkChallengeSignVerify(b *testing.B) { // BenchmarkPeerScoring measures KD-tree rebuild and peer selection. func BenchmarkPeerScoring(b *testing.B) { dir := b.TempDir() - reg, err := NewPeerRegistryWithPath(testJoinPath(dir, "peers.json")) + reg, err := NewPeerRegistryFromPath(testJoinPath(dir, "peers.json")) if err != nil { b.Fatalf("create registry: %v", err) } diff --git a/node/controller_test.go b/node/controller_test.go index 937bff4..4b3ba3e 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -24,7 +24,7 @@ func setupControllerPair(t *testing.T) (*Controller, *Worker, *testTransportPair // Server side: register a Worker to handle incoming requests. worker := NewWorker(tp.ServerNode, tp.Server) - worker.RegisterWithTransport() + worker.RegisterOnTransport() // Client side: create a Controller (registers handleResponse via OnMessage). controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -56,7 +56,7 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { u, _ := url.Parse(ts.URL) worker := NewWorker(nm, srv) - worker.RegisterWithTransport() + worker.RegisterOnTransport() t.Cleanup(func() { // Brief pause to let in-flight readLoop/Send operations finish before @@ -120,7 +120,7 @@ func TestController_AutoConnect_Good(t *testing.T) { // Register worker on the server side. worker := NewWorker(tp.ServerNode, tp.Server) - worker.RegisterWithTransport() + worker.RegisterOnTransport() // Create controller WITHOUT establishing a connection first. controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -432,7 +432,7 @@ func setupControllerPairWithMiner(t *testing.T) (*Controller, *Worker, *testTran }, } worker.SetMinerManager(mm) - worker.RegisterWithTransport() + worker.RegisterOnTransport() // Client side: create a Controller. controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -551,7 +551,7 @@ func TestController_StartRemoteMiner_NoIdentity_Bad(t *testing.T) { // Create a node without identity keyPath, configPath := testNodeManagerPaths(t.TempDir()) - nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) + nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -580,7 +580,7 @@ func TestController_StopRemoteMiner_NotFound_Bad(t *testing.T) { func TestController_StopRemoteMiner_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) - nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) + nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -613,7 +613,7 @@ func TestController_GetRemoteLogs_LimitedLines_Good(t *testing.T) { func TestController_GetRemoteLogs_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) - nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) + nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -640,7 +640,7 @@ func TestController_GetRemoteStats_WithMiners_Good(t *testing.T) { func TestController_GetRemoteStats_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) - nmNoID, err := NewNodeManagerWithPaths(keyPath, configPath) + nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) controller := NewController(nmNoID, tp.ClientReg, tp.Client) @@ -654,7 +654,7 @@ func TestController_ConnectToPeer_Success_Good(t *testing.T) { tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) - worker.RegisterWithTransport() + worker.RegisterOnTransport() controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) @@ -716,7 +716,7 @@ func TestController_HandleResponse_FullChannel_Ugly(t *testing.T) { func TestController_PingPeer_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) - nmNoID, _ := NewNodeManagerWithPaths(keyPath, configPath) + nmNoID, _ := NewNodeManagerFromPaths(keyPath, configPath) controller := NewController(nmNoID, tp.ClientReg, tp.Client) _, err := controller.PingPeer("some-peer") diff --git a/node/identity_test.go b/node/identity_test.go index 9693716..3a2ff9c 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -7,7 +7,7 @@ import ( // setupTestNodeManager creates a NodeManager with paths in a temp directory. func setupTestNodeManager(t *testing.T) (*NodeManager, func()) { tmpDir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -65,7 +65,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { keyPath, configPath := testNodeManagerPaths(tmpDir) // First, create an identity - nm1, err := NewNodeManagerWithPaths(keyPath, configPath) + nm1, err := NewNodeManagerFromPaths(keyPath, configPath) if err != nil { t.Fatalf("failed to create first node manager: %v", err) } @@ -79,7 +79,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { originalPubKey := nm1.GetIdentity().PublicKey // Create a new manager - should load existing identity - nm2, err := NewNodeManagerWithPaths(keyPath, configPath) + nm2, err := NewNodeManagerFromPaths(keyPath, configPath) if err != nil { t.Fatalf("failed to create second node manager: %v", err) } @@ -104,7 +104,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { tmpDir2 := t.TempDir() // Node 1 - nm1, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir1)) + nm1, err := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir1)) if err != nil { t.Fatalf("failed to create node manager 1: %v", err) } @@ -114,7 +114,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { } // Node 2 - nm2, err := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir2)) + nm2, err := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir2)) if err != nil { t.Fatalf("failed to create node manager 2: %v", err) } @@ -306,10 +306,10 @@ func TestIdentity_ChallengeResponse_Good(t *testing.T) { tmpDir1 := t.TempDir() tmpDir2 := t.TempDir() - nm1, _ := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir1)) + nm1, _ := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir1)) nm1.GenerateIdentity("challenger", RoleDual) - nm2, _ := NewNodeManagerWithPaths(testNodeManagerPaths(tmpDir2)) + nm2, _ := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir2)) nm2.GenerateIdentity("responder", RoleDual) // Challenger generates challenge @@ -355,7 +355,7 @@ func TestIdentity_NodeManager_GetIdentity_NilWhenNoIdentity_Bad(t *testing.T) { func TestIdentity_NodeManager_Delete_NoFiles_Bad(t *testing.T) { tmpDir := t.TempDir() - nm, err := NewNodeManagerWithPaths( + nm, err := NewNodeManagerFromPaths( testJoinPath(tmpDir, "nonexistent.key"), testJoinPath(tmpDir, "nonexistent.json"), ) diff --git a/node/integration_test.go b/node/integration_test.go index c31da5a..14f68b4 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -78,7 +78,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { }, }, }) - worker.RegisterWithTransport() + worker.RegisterOnTransport() // Start the worker transport behind httptest. mux := http.NewServeMux() @@ -346,7 +346,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { keyPath, configPath := testNodeManagerPaths(dir) // Create and persist identity. - nm1, err := NewNodeManagerWithPaths(keyPath, configPath) + nm1, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) require.NoError(t, nm1.GenerateIdentity("persistent-node", RoleDual)) @@ -354,7 +354,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { require.NotNil(t, original) // Reload from disk. - nm2, err := NewNodeManagerWithPaths(keyPath, configPath) + nm2, err := NewNodeManagerFromPaths(keyPath, configPath) require.NoError(t, err) require.True(t, nm2.HasIdentity(), "identity should be loaded from disk") @@ -383,7 +383,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { // stmfGenerateKeyPair is a helper that generates a keypair and returns // the public key as base64 (for use in DeriveSharedSecret tests). func stmfGenerateKeyPair(dir string) (string, error) { - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { return "", err } @@ -526,7 +526,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig()) worker := NewWorker(workerNM, workerTransport) - worker.RegisterWithTransport() + worker.RegisterOnTransport() mux := http.NewServeMux() mux.HandleFunc("/ws", workerTransport.handleWSUpgrade) @@ -657,7 +657,7 @@ func TestIntegration_GetRemoteStats_EndToEnd_Good(t *testing.T) { tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) - worker.RegisterWithTransport() + worker.RegisterOnTransport() controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) diff --git a/node/peer_test.go b/node/peer_test.go index 9b0d506..a8ce4d5 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -10,7 +10,7 @@ func setupTestPeerRegistry(t *testing.T) (*PeerRegistry, func()) { tmpDir := t.TempDir() peersPath := testJoinPath(tmpDir, "peers.json") - pr, err := NewPeerRegistryWithPath(peersPath) + pr, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -352,7 +352,7 @@ func TestPeer_Registry_Persistence_Good(t *testing.T) { peersPath := testJoinPath(tmpDir, "peers.json") // Create and save - pr1, err := NewPeerRegistryWithPath(peersPath) + pr1, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create first registry: %v", err) } @@ -373,7 +373,7 @@ func TestPeer_Registry_Persistence_Good(t *testing.T) { } // Load in new registry from same path - pr2, err := NewPeerRegistryWithPath(peersPath) + pr2, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create second registry: %v", err) } @@ -822,7 +822,7 @@ func TestPeer_Registry_Close_NoDirtyData_Ugly(t *testing.T) { func TestPeer_Registry_Close_WithDirtyData_Ugly(t *testing.T) { tmpDir := t.TempDir() peersPath := testJoinPath(tmpDir, "peers.json") - pr, err := NewPeerRegistryWithPath(peersPath) + pr, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) } @@ -837,7 +837,7 @@ func TestPeer_Registry_Close_WithDirtyData_Ugly(t *testing.T) { } // Verify data was saved - pr2, err := NewPeerRegistryWithPath(peersPath) + pr2, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to reload: %v", err) } @@ -849,7 +849,7 @@ func TestPeer_Registry_Close_WithDirtyData_Ugly(t *testing.T) { func TestPeer_Registry_ScheduleSave_Debounce_Ugly(t *testing.T) { tmpDir := t.TempDir() peersPath := testJoinPath(tmpDir, "peers.json") - pr, err := NewPeerRegistryWithPath(peersPath) + pr, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) } @@ -869,7 +869,7 @@ func TestPeer_Registry_ScheduleSave_Debounce_Ugly(t *testing.T) { func TestPeer_Registry_SaveNow_Good(t *testing.T) { tmpDir := t.TempDir() peersPath := testJoinPath(tmpDir, "subdir", "peers.json") - pr, err := NewPeerRegistryWithPath(peersPath) + pr, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) } @@ -897,7 +897,7 @@ func TestPeer_Registry_ScheduleSave_TimerFires_Ugly(t *testing.T) { tmpDir := t.TempDir() peersPath := testJoinPath(tmpDir, "peers.json") - pr, err := NewPeerRegistryWithPath(peersPath) + pr, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to create registry: %v", err) } @@ -913,7 +913,7 @@ func TestPeer_Registry_ScheduleSave_TimerFires_Ugly(t *testing.T) { } // Reload and verify - pr2, err := NewPeerRegistryWithPath(peersPath) + pr2, err := NewPeerRegistryFromPath(peersPath) if err != nil { t.Fatalf("failed to reload: %v", err) } diff --git a/node/transport_test.go b/node/transport_test.go index 21368b7..d278ac1 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -19,7 +19,7 @@ import ( func testNode(t *testing.T, name string, role NodeRole) *NodeManager { t.Helper() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("create node manager %q: %v", name, err) } @@ -33,7 +33,7 @@ func testNode(t *testing.T, name string, role NodeRole) *NodeManager { func testRegistry(t *testing.T) *PeerRegistry { t.Helper() dir := t.TempDir() - reg, err := NewPeerRegistryWithPath(testJoinPath(dir, "peers.json")) + reg, err := NewPeerRegistryFromPath(testJoinPath(dir, "peers.json")) if err != nil { t.Fatalf("create registry: %v", err) } diff --git a/node/worker_test.go b/node/worker_test.go index 7244abc..ccea789 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -21,7 +21,7 @@ func TestWorker_NewWorker_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -29,7 +29,7 @@ func TestWorker_NewWorker_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -54,7 +54,7 @@ func TestWorker_SetMinerManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -62,7 +62,7 @@ func TestWorker_SetMinerManager_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -84,7 +84,7 @@ func TestWorker_SetProfileManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -92,7 +92,7 @@ func TestWorker_SetProfileManager_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -114,7 +114,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -122,7 +122,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -175,7 +175,7 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -183,7 +183,7 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -235,7 +235,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -243,7 +243,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -275,7 +275,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -283,7 +283,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -315,7 +315,7 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -323,7 +323,7 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -355,7 +355,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -363,7 +363,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -399,7 +399,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -407,7 +407,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -582,7 +582,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } @@ -590,7 +590,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -751,14 +751,14 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -815,14 +815,14 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -922,14 +922,14 @@ func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -983,14 +983,14 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1010,14 +1010,14 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1065,14 +1065,14 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1111,14 +1111,14 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1175,14 +1175,14 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1231,14 +1231,14 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, err := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil { t.Fatalf("failed to generate identity: %v", err) } - pr, err := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) if err != nil { t.Fatalf("failed to create peer registry: %v", err) } @@ -1292,9 +1292,9 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { defer cleanup() dir := t.TempDir() - nm, _ := NewNodeManagerWithPaths(testNodeManagerPaths(dir)) + nm, _ := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) nm.GenerateIdentity("test", RoleWorker) - pr, _ := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() @@ -1314,12 +1314,12 @@ func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { defer cleanup() tmpDir := t.TempDir() - nm, _ := NewNodeManagerWithPaths( + nm, _ := NewNodeManagerFromPaths( testJoinPath(tmpDir, "priv.key"), testJoinPath(tmpDir, "node.json"), ) // Don't generate identity - pr, _ := NewPeerRegistryWithPath(testJoinPath(t.TempDir(), "peers.json")) + pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() @@ -1337,7 +1337,7 @@ func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) { worker := NewWorker(tp.ServerNode, tp.Server) // No miner manager set -- start_miner will fail and send error response - worker.RegisterWithTransport() + worker.RegisterOnTransport() controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) tp.connectClient(t) @@ -1376,7 +1376,7 @@ func TestWorker_HandleMessage_GetStats_IntegrationViaWebSocket_Good(t *testing.T }, } worker.SetMinerManager(mm) - worker.RegisterWithTransport() + worker.RegisterOnTransport() controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) tp.connectClient(t) -- 2.45.3 From cbb3f011762872cba5c7e40f9423c034edb2d351 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:40:59 +0000 Subject: [PATCH 14/60] refactor(node): rename test helpers for AX clarity Co-Authored-By: Virgil --- node/controller_test.go | 12 +++++----- node/integration_test.go | 40 +++++++++++++++++----------------- node/transport_test.go | 42 +++++++++++++++++------------------ node/worker_test.go | 47 ++++++++++++++++++++-------------------- 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/node/controller_test.go b/node/controller_test.go index 4b3ba3e..6077cce 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -44,8 +44,8 @@ func setupControllerPair(t *testing.T) (*Controller, *Worker, *testTransportPair func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { t.Helper() - nm := testNode(t, "worker", RoleWorker) - reg := testRegistry(t) + nm := newTestNodeManager(t, "worker", RoleWorker) + reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() srv := NewTransport(nm, reg, cfg) @@ -149,8 +149,8 @@ func TestController_AutoConnect_Good(t *testing.T) { func TestController_GetAllStats_Good(t *testing.T) { // Controller node with connections to two independent worker servers. - controllerNM := testNode(t, "controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "controller", RoleController) + controllerReg := newTestPeerRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) t.Cleanup(func() { controllerTransport.Stop() }) @@ -218,8 +218,8 @@ func TestController_PingPeerRTT_Good(t *testing.T) { func TestController_ConcurrentRequests_Ugly(t *testing.T) { // Multiple goroutines send pings to different peers simultaneously. // Verify correct correlation — no cross-talk between responses. - controllerNM := testNode(t, "controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "controller", RoleController) + controllerReg := newTestPeerRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) t.Cleanup(func() { controllerTransport.Stop() }) diff --git a/node/integration_test.go b/node/integration_test.go index 14f68b4..46cf730 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -31,8 +31,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // ---------------------------------------------------------------- // Step 1: Identity creation // ---------------------------------------------------------------- - controllerNM := testNode(t, "integration-controller", RoleController) - workerNM := testNode(t, "integration-worker", RoleWorker) + controllerNM := newTestNodeManager(t, "integration-controller", RoleController) + workerNM := newTestNodeManager(t, "integration-worker", RoleWorker) controllerIdentity := controllerNM.GetIdentity() workerIdentity := workerNM.GetIdentity() @@ -48,8 +48,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // ---------------------------------------------------------------- // Step 2: Set up transports, registries, worker, and controller // ---------------------------------------------------------------- - workerReg := testRegistry(t) - controllerReg := testRegistry(t) + workerReg := newTestPeerRegistry(t) + controllerReg := newTestPeerRegistry(t) workerCfg := DefaultTransportConfig() workerCfg.PingInterval = 2 * time.Second @@ -239,8 +239,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // TestIntegration_SharedSecretAgreement verifies that two independently created // nodes derive the same shared secret via ECDH. func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { - nodeA := testNode(t, "secret-node-a", RoleDual) - nodeB := testNode(t, "secret-node-b", RoleDual) + nodeA := newTestNodeManager(t, "secret-node-a", RoleDual) + nodeB := newTestNodeManager(t, "secret-node-b", RoleDual) pubKeyA := nodeA.GetIdentity().PublicKey pubKeyB := nodeB.GetIdentity().PublicKey @@ -284,8 +284,8 @@ func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { // TestIntegration_MultiPeerTopology verifies that a controller can // simultaneously communicate with multiple workers. func TestIntegration_MultiPeerTopology_Good(t *testing.T) { - controllerNM := testNode(t, "multi-controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "multi-controller", RoleController) + controllerReg := newTestPeerRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) t.Cleanup(func() { controllerTransport.Stop() }) @@ -396,8 +396,8 @@ func stmfGenerateKeyPair(dir string) (string, error) { // TestIntegration_UEPSFullRoundTrip exercises a complete UEPS packet // lifecycle: build, sign, transmit (simulated), read, verify, dispatch. func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { - nodeA := testNode(t, "ueps-node-a", RoleController) - nodeB := testNode(t, "ueps-node-b", RoleWorker) + nodeA := newTestNodeManager(t, "ueps-node-a", RoleController) + nodeB := newTestNodeManager(t, "ueps-node-b", RoleWorker) bPubKey := nodeB.GetIdentity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) @@ -447,8 +447,8 @@ func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { // TestIntegration_UEPSIntegrityFailure verifies that a tampered UEPS packet // is rejected by HMAC verification. func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { - nodeA := testNode(t, "integrity-a", RoleController) - nodeB := testNode(t, "integrity-b", RoleWorker) + nodeA := newTestNodeManager(t, "integrity-a", RoleController) + nodeB := newTestNodeManager(t, "integrity-b", RoleWorker) bPubKey := nodeB.GetIdentity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) @@ -478,8 +478,8 @@ func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { // TestIntegration_AllowlistHandshakeRejection verifies that a peer not in the // allowlist is rejected during the WebSocket handshake. func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { - workerNM := testNode(t, "allowlist-worker", RoleWorker) - workerReg := testRegistry(t) + workerNM := newTestNodeManager(t, "allowlist-worker", RoleWorker) + workerReg := newTestPeerRegistry(t) workerReg.SetAuthMode(PeerAuthAllowlist) workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig()) @@ -494,8 +494,8 @@ func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { u, _ := url.Parse(ts.URL) - controllerNM := testNode(t, "rejected-controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "rejected-controller", RoleController) + controllerReg := newTestPeerRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) t.Cleanup(func() { controllerTransport.Stop() }) @@ -515,12 +515,12 @@ func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { // TestIntegration_AllowlistHandshakeAccepted verifies that an allowlisted // peer can connect successfully. func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { - workerNM := testNode(t, "allowlist-worker-ok", RoleWorker) - workerReg := testRegistry(t) + workerNM := newTestNodeManager(t, "allowlist-worker-ok", RoleWorker) + workerReg := newTestPeerRegistry(t) workerReg.SetAuthMode(PeerAuthAllowlist) - controllerNM := testNode(t, "allowed-controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "allowed-controller", RoleController) + controllerReg := newTestPeerRegistry(t) workerReg.AllowPublicKey(controllerNM.GetIdentity().PublicKey) diff --git a/node/transport_test.go b/node/transport_test.go index d278ac1..4fe6e1f 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -15,8 +15,7 @@ import ( // --- Test Helpers --- -// testNode creates a NodeManager with a generated identity in a temp directory. -func testNode(t *testing.T, name string, role NodeRole) *NodeManager { +func newTestNodeManager(t *testing.T, name string, role NodeRole) *NodeManager { t.Helper() dir := t.TempDir() nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir)) @@ -29,8 +28,7 @@ func testNode(t *testing.T, name string, role NodeRole) *NodeManager { return nm } -// testRegistry creates a PeerRegistry with open auth in a temp directory. -func testRegistry(t *testing.T) *PeerRegistry { +func newTestPeerRegistry(t *testing.T) *PeerRegistry { t.Helper() dir := t.TempDir() reg, err := NewPeerRegistryFromPath(testJoinPath(dir, "peers.json")) @@ -63,10 +61,10 @@ func setupTestTransportPair(t *testing.T) *testTransportPair { func setupTestTransportPairWithConfig(t *testing.T, serverCfg, clientCfg TransportConfig) *testTransportPair { t.Helper() - serverNM := testNode(t, "server", RoleWorker) - clientNM := testNode(t, "client", RoleController) - serverReg := testRegistry(t) - clientReg := testRegistry(t) + serverNM := newTestNodeManager(t, "server", RoleWorker) + clientNM := newTestNodeManager(t, "client", RoleController) + serverReg := newTestPeerRegistry(t) + clientReg := newTestPeerRegistry(t) serverTransport := NewTransport(serverNM, serverReg, serverCfg) clientTransport := NewTransport(clientNM, clientReg, clientCfg) @@ -410,8 +408,8 @@ func TestTransport_RateLimiting_Good(t *testing.T) { func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { // Server with MaxConns=1 - serverNM := testNode(t, "maxconns-server", RoleWorker) - serverReg := testRegistry(t) + serverNM := newTestNodeManager(t, "maxconns-server", RoleWorker) + serverReg := newTestPeerRegistry(t) serverCfg := DefaultTransportConfig() serverCfg.MaxConns = 1 @@ -429,8 +427,8 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { serverAddr := u.Host // First client connects successfully - client1NM := testNode(t, "client1", RoleController) - client1Reg := testRegistry(t) + client1NM := newTestNodeManager(t, "client1", RoleController) + client1Reg := newTestPeerRegistry(t) client1Transport := NewTransport(client1NM, client1Reg, DefaultTransportConfig()) t.Cleanup(func() { client1Transport.Stop() }) @@ -446,8 +444,8 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { time.Sleep(50 * time.Millisecond) // Second client should be rejected (MaxConns=1 reached) - client2NM := testNode(t, "client2", RoleController) - client2Reg := testRegistry(t) + client2NM := newTestNodeManager(t, "client2", RoleController) + client2Reg := newTestPeerRegistry(t) client2Transport := NewTransport(client2NM, client2Reg, DefaultTransportConfig()) t.Cleanup(func() { client2Transport.Stop() }) @@ -586,8 +584,8 @@ func TestTransport_ConcurrentSends_Ugly(t *testing.T) { func TestTransport_Broadcast_Good(t *testing.T) { // Set up a controller with two worker peers connected. - controllerNM := testNode(t, "broadcast-controller", RoleController) - controllerReg := testRegistry(t) + controllerNM := newTestNodeManager(t, "broadcast-controller", RoleController) + controllerReg := newTestPeerRegistry(t) controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig()) t.Cleanup(func() { controllerTransport.Stop() }) @@ -669,8 +667,8 @@ func TestTransport_BroadcastExcludesSender_Good(t *testing.T) { } func TestTransport_NewTransport_DefaultMaxMessageSize_Good(t *testing.T) { - nm := testNode(t, "defaults", RoleWorker) - reg := testRegistry(t) + nm := newTestNodeManager(t, "defaults", RoleWorker) + reg := newTestPeerRegistry(t) cfg := TransportConfig{ MaxMessageSize: 0, // should use default } @@ -701,8 +699,8 @@ func TestTransport_ConnectedPeers_Good(t *testing.T) { } func TestTransport_StartAndStop_Good(t *testing.T) { - nm := testNode(t, "start-test", RoleWorker) - reg := testRegistry(t) + nm := newTestNodeManager(t, "start-test", RoleWorker) + reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() cfg.ListenAddr = ":0" // Let OS pick a free port @@ -723,8 +721,8 @@ func TestTransport_StartAndStop_Good(t *testing.T) { } func TestTransport_CheckOrigin_Good(t *testing.T) { - nm := testNode(t, "origin-test", RoleWorker) - reg := testRegistry(t) + nm := newTestNodeManager(t, "origin-test", RoleWorker) + reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() tr := NewTransport(nm, reg, cfg) diff --git a/node/worker_test.go b/node/worker_test.go index ccea789..b29e2a8 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -8,8 +8,7 @@ import ( core "dappco.re/go/core" ) -// setupTestEnv sets up a temporary environment for testing and returns cleanup function -func setupTestEnv(t *testing.T) func() { +func setupTestEnvironment(t *testing.T) func() { tmpDir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", testJoinPath(tmpDir, "config")) t.Setenv("XDG_DATA_HOME", testJoinPath(tmpDir, "data")) @@ -17,7 +16,7 @@ func setupTestEnv(t *testing.T) func() { } func TestWorker_NewWorker_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -50,7 +49,7 @@ func TestWorker_NewWorker_Good(t *testing.T) { } func TestWorker_SetMinerManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -80,7 +79,7 @@ func TestWorker_SetMinerManager_Good(t *testing.T) { } func TestWorker_SetProfileManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -110,7 +109,7 @@ func TestWorker_SetProfileManager_Good(t *testing.T) { } func TestWorker_HandlePing_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -171,7 +170,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { } func TestWorker_HandleGetStats_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -231,7 +230,7 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { } func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -271,7 +270,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { } func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -311,7 +310,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { } func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -351,7 +350,7 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { } func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -395,7 +394,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { } func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -578,7 +577,7 @@ func (m *mockProfileManagerFailing) SaveProfile(profile any) error { } func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -747,7 +746,7 @@ func (m *mockMinerManagerWithStart) StartMiner(minerType string, config any) (Mi } func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -811,7 +810,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { } func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -918,7 +917,7 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { } func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -979,7 +978,7 @@ func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { } func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1006,7 +1005,7 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { } func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1061,7 +1060,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { } func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1107,7 +1106,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { } func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1171,7 +1170,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { } func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1227,7 +1226,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { } func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1288,7 +1287,7 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { } func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() dir := t.TempDir() @@ -1310,7 +1309,7 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { } func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { - cleanup := setupTestEnv(t) + cleanup := setupTestEnvironment(t) defer cleanup() tmpDir := t.TempDir() -- 2.45.3 From 48cd87e081b13fb02e3e3e5a046afd395297d3be Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:45:55 +0000 Subject: [PATCH 15/60] refactor(node): clarify remaining AX fixture names Co-Authored-By: Virgil --- node/ax_test_helpers_test.go | 2 +- node/bundle.go | 2 +- node/core_fs.go | 12 ++++++------ node/identity_test.go | 20 +++++++------------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/node/ax_test_helpers_test.go b/node/ax_test_helpers_test.go index 5b2051e..f990651 100644 --- a/node/ax_test_helpers_test.go +++ b/node/ax_test_helpers_test.go @@ -20,7 +20,7 @@ func testNodeManagerPaths(dir string) (string, string) { func testWriteFile(t *testing.T, path string, content []byte, mode fs.FileMode) { t.Helper() - require.NoError(t, fsResultErr(localFS.WriteMode(path, string(content), mode))) + require.NoError(t, fsResultError(localFS.WriteMode(path, string(content), mode))) } func testReadFile(t *testing.T, path string) []byte { diff --git a/node/bundle.go b/node/bundle.go index 8b42afa..d4f3409 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -349,7 +349,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { fsDelete(fullPath) return "", core.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) } - if err := fsResultErr(localFS.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { + if err := fsResultError(localFS.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) } diff --git a/node/core_fs.go b/node/core_fs.go index f7056c8..6a6af3f 100644 --- a/node/core_fs.go +++ b/node/core_fs.go @@ -7,17 +7,17 @@ import core "dappco.re/go/core" var localFS = (&core.Fs{}).New("/") func fsEnsureDir(path string) error { - return fsResultErr(localFS.EnsureDir(path)) + return fsResultError(localFS.EnsureDir(path)) } func fsWrite(path, content string) error { - return fsResultErr(localFS.Write(path, content)) + return fsResultError(localFS.Write(path, content)) } func fsRead(path string) (string, error) { result := localFS.Read(path) if !result.OK { - return "", fsResultErr(result) + return "", fsResultError(result) } content, ok := result.Value.(string) @@ -29,18 +29,18 @@ func fsRead(path string) (string, error) { } func fsDelete(path string) error { - return fsResultErr(localFS.Delete(path)) + return fsResultError(localFS.Delete(path)) } func fsRename(oldPath, newPath string) error { - return fsResultErr(localFS.Rename(oldPath, newPath)) + return fsResultError(localFS.Rename(oldPath, newPath)) } func fsExists(path string) bool { return localFS.Exists(path) } -func fsResultErr(result core.Result) error { +func fsResultError(result core.Result) error { if result.OK { return nil } diff --git a/node/identity_test.go b/node/identity_test.go index 3a2ff9c..ad8820f 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -4,21 +4,19 @@ import ( "testing" ) -// setupTestNodeManager creates a NodeManager with paths in a temp directory. -func setupTestNodeManager(t *testing.T) (*NodeManager, func()) { +func newTestNodeManagerWithoutIdentity(t *testing.T) *NodeManager { tmpDir := t.TempDir() nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(tmpDir)) if err != nil { t.Fatalf("failed to create node manager: %v", err) } - return nm, func() {} + return nm } func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Run("NewNodeManager", func(t *testing.T) { - nm, cleanup := setupTestNodeManager(t) - defer cleanup() + nm := newTestNodeManagerWithoutIdentity(t) if nm.HasIdentity() { t.Error("new node manager should not have identity") @@ -26,8 +24,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { }) t.Run("GenerateIdentity", func(t *testing.T) { - nm, cleanup := setupTestNodeManager(t) - defer cleanup() + nm := newTestNodeManagerWithoutIdentity(t) err := nm.GenerateIdentity("test-node", RoleDual) if err != nil { @@ -147,8 +144,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { }) t.Run("DeleteIdentity", func(t *testing.T) { - nm, cleanup := setupTestNodeManager(t) - defer cleanup() + nm := newTestNodeManagerWithoutIdentity(t) err := nm.GenerateIdentity("delete-me", RoleDual) if err != nil { @@ -333,8 +329,7 @@ func TestIdentity_ChallengeResponse_Good(t *testing.T) { } func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { - nm, cleanup := setupTestNodeManager(t) - defer cleanup() + nm := newTestNodeManagerWithoutIdentity(t) // No identity generated _, err := nm.DeriveSharedSecret("some-key") @@ -344,8 +339,7 @@ func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { } func TestIdentity_NodeManager_GetIdentity_NilWhenNoIdentity_Bad(t *testing.T) { - nm, cleanup := setupTestNodeManager(t) - defer cleanup() + nm := newTestNodeManagerWithoutIdentity(t) identity := nm.GetIdentity() if identity != nil { -- 2.45.3 From ca885ff386969641691bc65d90afbd9caabefa3a Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 20:52:19 +0000 Subject: [PATCH 16/60] refactor(node): clarify AX filesystem and message names Co-Authored-By: Virgil --- logging/logger.go | 52 ++++++++++++++++++------------------ node/ax_test_helpers_test.go | 4 +-- node/bundle.go | 12 ++++----- node/bundle_test.go | 2 +- node/core_fs.go | 34 ++++++++++++----------- node/identity.go | 22 +++++++-------- node/message.go | 22 +++++++-------- node/peer.go | 12 ++++----- node/peer_test.go | 4 +-- 9 files changed, 83 insertions(+), 81 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index e7e0858..fccdae2 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -55,7 +55,7 @@ type Logger struct { // Config holds configuration for creating a new Logger. // -// cfg := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} +// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} type Config struct { Output io.Writer Level Level @@ -64,7 +64,7 @@ type Config struct { // DefaultConfig returns the default logger configuration. // -// cfg := DefaultConfig() +// config := DefaultConfig() func DefaultConfig() Config { return Config{ Output: defaultOutput, @@ -76,14 +76,14 @@ func DefaultConfig() Config { // New creates a logger from an explicit configuration. // // logger := New(DefaultConfig()) -func New(cfg Config) *Logger { - if cfg.Output == nil { - cfg.Output = defaultOutput +func New(config Config) *Logger { + if config.Output == nil { + config.Output = defaultOutput } return &Logger{ - output: cfg.Output, - level: cfg.Level, - component: cfg.Component, + output: config.Output, + level: config.Level, + component: config.Component, } } @@ -144,7 +144,7 @@ func (stderrWriter) Write(p []byte) (int, error) { var defaultOutput io.Writer = stderrWriter{} // log writes a log message at the specified level. -func (l *Logger) log(level Level, msg string, fields Fields) { +func (l *Logger) log(level Level, message string, fields Fields) { l.mu.Lock() defer l.mu.Unlock() @@ -167,7 +167,7 @@ func (l *Logger) log(level Level, msg string, fields Fields) { } sb.WriteString(" ") - sb.WriteString(msg) + sb.WriteString(message) // Add fields if present if len(fields) > 0 { @@ -185,23 +185,23 @@ func (l *Logger) log(level Level, msg string, fields Fields) { } // Debug logs a debug message. -func (l *Logger) Debug(msg string, fields ...Fields) { - l.log(LevelDebug, msg, mergeFields(fields)) +func (l *Logger) Debug(message string, fields ...Fields) { + l.log(LevelDebug, message, mergeFields(fields)) } // Info logs an informational message. -func (l *Logger) Info(msg string, fields ...Fields) { - l.log(LevelInfo, msg, mergeFields(fields)) +func (l *Logger) Info(message string, fields ...Fields) { + l.log(LevelInfo, message, mergeFields(fields)) } // Warn logs a warning message. -func (l *Logger) Warn(msg string, fields ...Fields) { - l.log(LevelWarn, msg, mergeFields(fields)) +func (l *Logger) Warn(message string, fields ...Fields) { + l.log(LevelWarn, message, mergeFields(fields)) } // Error logs an error message. -func (l *Logger) Error(msg string, fields ...Fields) { - l.log(LevelError, msg, mergeFields(fields)) +func (l *Logger) Error(message string, fields ...Fields) { + l.log(LevelError, message, mergeFields(fields)) } // Debugf logs a formatted debug message. @@ -280,29 +280,29 @@ func SetGlobalLevel(level Level) { // Debug logs a debug message using the global logger. // // Debug("connected", Fields{"peer_id": "node-1"}) -func Debug(msg string, fields ...Fields) { - Global().Debug(msg, fields...) +func Debug(message string, fields ...Fields) { + Global().Debug(message, fields...) } // Info logs an informational message using the global logger. // // Info("worker started", Fields{"component": "transport"}) -func Info(msg string, fields ...Fields) { - Global().Info(msg, fields...) +func Info(message string, fields ...Fields) { + Global().Info(message, fields...) } // Warn logs a warning message using the global logger. // // Warn("peer rate limited", Fields{"peer_id": "node-1"}) -func Warn(msg string, fields ...Fields) { - Global().Warn(msg, fields...) +func Warn(message string, fields ...Fields) { + Global().Warn(message, fields...) } // Error logs an error message using the global logger. // // Error("send failed", Fields{"peer_id": "node-1"}) -func Error(msg string, fields ...Fields) { - Global().Error(msg, fields...) +func Error(message string, fields ...Fields) { + Global().Error(message, fields...) } // Debugf logs a formatted debug message using the global logger. diff --git a/node/ax_test_helpers_test.go b/node/ax_test_helpers_test.go index f990651..067165a 100644 --- a/node/ax_test_helpers_test.go +++ b/node/ax_test_helpers_test.go @@ -20,12 +20,12 @@ func testNodeManagerPaths(dir string) (string, string) { func testWriteFile(t *testing.T, path string, content []byte, mode fs.FileMode) { t.Helper() - require.NoError(t, fsResultError(localFS.WriteMode(path, string(content), mode))) + require.NoError(t, filesystemResultError(localFileSystem.WriteMode(path, string(content), mode))) } func testReadFile(t *testing.T, path string) []byte { t.Helper() - content, err := fsRead(path) + content, err := filesystemRead(path) require.NoError(t, err) return []byte(content) } diff --git a/node/bundle.go b/node/bundle.go index d4f3409..edd725d 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -97,7 +97,7 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e // bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary - minerContent, err := fsRead(minerPath) + minerContent, err := filesystemRead(minerPath) if err != nil { return nil, core.E("CreateMinerBundle", "failed to read miner binary", err) } @@ -286,7 +286,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { absDestDir = core.CleanPath(absDestDir, pathSeparator) } - if err := fsEnsureDir(absDestDir); err != nil { + if err := filesystemEnsureDir(absDestDir); err != nil { return "", err } @@ -329,12 +329,12 @@ func extractTarball(tarData []byte, destDir string) (string, error) { switch hdr.Typeflag { case tar.TypeDir: - if err := fsEnsureDir(fullPath); err != nil { + if err := filesystemEnsureDir(fullPath); err != nil { return "", err } case tar.TypeReg: // Ensure parent directory exists - if err := fsEnsureDir(core.PathDir(fullPath)); err != nil { + if err := filesystemEnsureDir(core.PathDir(fullPath)); err != nil { return "", err } @@ -346,10 +346,10 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return "", core.E("extractTarball", "failed to write file "+hdr.Name, err) } if int64(len(content)) > maxFileSize { - fsDelete(fullPath) + filesystemDelete(fullPath) return "", core.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) } - if err := fsResultError(localFS.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { + if err := filesystemResultError(localFileSystem.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) } diff --git a/node/bundle_test.go b/node/bundle_test.go index 8d04966..59d22a5 100644 --- a/node/bundle_test.go +++ b/node/bundle_test.go @@ -430,7 +430,7 @@ func TestBundle_ExtractTarball_PathTraversal_Bad(t *testing.T) { // Verify symlink was not created linkPath := testJoinPath(tmpDir, "link") - if fsExists(linkPath) { + if filesystemExists(linkPath) { t.Error("symlink should not be created") } }) diff --git a/node/core_fs.go b/node/core_fs.go index 6a6af3f..2deb2dd 100644 --- a/node/core_fs.go +++ b/node/core_fs.go @@ -4,43 +4,45 @@ package node import core "dappco.re/go/core" -var localFS = (&core.Fs{}).New("/") +// localFileSystem is the package-scoped filesystem rooted at `/` so node code +// can use Core file operations without os helpers. +var localFileSystem = (&core.Fs{}).New("/") -func fsEnsureDir(path string) error { - return fsResultError(localFS.EnsureDir(path)) +func filesystemEnsureDir(path string) error { + return filesystemResultError(localFileSystem.EnsureDir(path)) } -func fsWrite(path, content string) error { - return fsResultError(localFS.Write(path, content)) +func filesystemWrite(path, content string) error { + return filesystemResultError(localFileSystem.Write(path, content)) } -func fsRead(path string) (string, error) { - result := localFS.Read(path) +func filesystemRead(path string) (string, error) { + result := localFileSystem.Read(path) if !result.OK { - return "", fsResultError(result) + return "", filesystemResultError(result) } content, ok := result.Value.(string) if !ok { - return "", core.E("node.fsRead", "filesystem read returned non-string content", nil) + return "", core.E("node.filesystemRead", "filesystem read returned non-string content", nil) } return content, nil } -func fsDelete(path string) error { - return fsResultError(localFS.Delete(path)) +func filesystemDelete(path string) error { + return filesystemResultError(localFileSystem.Delete(path)) } -func fsRename(oldPath, newPath string) error { - return fsResultError(localFS.Rename(oldPath, newPath)) +func filesystemRename(oldPath, newPath string) error { + return filesystemResultError(localFileSystem.Rename(oldPath, newPath)) } -func fsExists(path string) bool { - return localFS.Exists(path) +func filesystemExists(path string) bool { + return localFileSystem.Exists(path) } -func fsResultError(result core.Result) error { +func filesystemResultError(result core.Result) error { if result.OK { return nil } diff --git a/node/identity.go b/node/identity.go index 2f82de3..0198c3b 100644 --- a/node/identity.go +++ b/node/identity.go @@ -115,7 +115,7 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { } // Missing files indicate a first run; anything else is a load failure. - if !fsExists(keyPath) && !fsExists(configPath) { + if !filesystemExists(keyPath) && !filesystemExists(configPath) { return nm, nil } @@ -237,12 +237,12 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error func (n *NodeManager) savePrivateKey() error { // Ensure directory exists dir := core.PathDir(n.keyPath) - if err := fsEnsureDir(dir); err != nil { + if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } // Write private key - if err := fsWrite(n.keyPath, string(n.privateKey)); err != nil { + if err := filesystemWrite(n.keyPath, string(n.privateKey)); err != nil { return core.E("NodeManager.savePrivateKey", "failed to write private key", err) } @@ -253,7 +253,7 @@ func (n *NodeManager) savePrivateKey() error { func (n *NodeManager) saveIdentity() error { // Ensure directory exists dir := core.PathDir(n.configPath) - if err := fsEnsureDir(dir); err != nil { + if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.saveIdentity", "failed to create config directory", err) } @@ -263,7 +263,7 @@ func (n *NodeManager) saveIdentity() error { } data := result.Value.([]byte) - if err := fsWrite(n.configPath, string(data)); err != nil { + if err := filesystemWrite(n.configPath, string(data)); err != nil { return core.E("NodeManager.saveIdentity", "failed to write identity", err) } @@ -273,7 +273,7 @@ func (n *NodeManager) saveIdentity() error { // loadIdentity loads the node identity from disk. func (n *NodeManager) loadIdentity() error { // Load identity config - content, err := fsRead(n.configPath) + content, err := filesystemRead(n.configPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read identity", err) } @@ -285,7 +285,7 @@ func (n *NodeManager) loadIdentity() error { } // Load private key - keyContent, err := fsRead(n.keyPath) + keyContent, err := filesystemRead(n.keyPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read private key", err) } @@ -312,15 +312,15 @@ func (n *NodeManager) Delete() error { defer n.mu.Unlock() // Remove private key (ignore if already absent) - if fsExists(n.keyPath) { - if err := fsDelete(n.keyPath); err != nil { + if filesystemExists(n.keyPath) { + if err := filesystemDelete(n.keyPath); err != nil { return core.E("NodeManager.Delete", "failed to remove private key", err) } } // Remove identity config (ignore if already absent) - if fsExists(n.configPath) { - if err := fsDelete(n.configPath); err != nil { + if filesystemExists(n.configPath) { + if err := filesystemDelete(n.configPath); err != nil { return core.E("NodeManager.Delete", "failed to remove identity", err) } } diff --git a/node/message.go b/node/message.go index 5617886..98f474f 100644 --- a/node/message.go +++ b/node/message.go @@ -60,7 +60,7 @@ func IsProtocolVersionSupported(version string) bool { // MessageType defines the type of P2P message. // -// msgType := MsgPing +// messageType := MsgPing type MessageType string const ( @@ -92,7 +92,7 @@ const ( // Message represents a P2P message between nodes. // -// msg, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) +// message, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -105,8 +105,8 @@ type Message struct { // NewMessage builds a message with a generated ID and timestamp. // -// msg, err := NewMessage(MsgPing, "controller", "worker-1", PingPayload{SentAt: 42}) -func NewMessage(msgType MessageType, from, to string, payload any) (*Message, error) { +// message, err := NewMessage(MsgPing, "controller", "worker-1", PingPayload{SentAt: 42}) +func NewMessage(messageType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { data, err := MarshalJSON(payload) @@ -118,7 +118,7 @@ func NewMessage(msgType MessageType, from, to string, payload any) (*Message, er return &Message{ ID: uuid.New().String(), - Type: msgType, + Type: messageType, From: from, To: to, Timestamp: time.Now(), @@ -128,9 +128,9 @@ func NewMessage(msgType MessageType, from, to string, payload any) (*Message, er // Reply creates a response message that points back to the original. // -// reply, err := msg.Reply(MsgPong, PongPayload{SentAt: 42, ReceivedAt: 43}) -func (m *Message) Reply(msgType MessageType, payload any) (*Message, error) { - reply, err := NewMessage(msgType, m.To, m.From, payload) +// reply, err := message.Reply(MsgPong, PongPayload{SentAt: 42, ReceivedAt: 43}) +func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) { + reply, err := NewMessage(messageType, m.To, m.From, payload) if err != nil { return nil, err } @@ -141,12 +141,12 @@ func (m *Message) Reply(msgType MessageType, payload any) (*Message, error) { // ParsePayload decodes the payload into the supplied target. // // var ping PingPayload -// err := msg.ParsePayload(&ping) -func (m *Message) ParsePayload(v any) error { +// err := message.ParsePayload(&ping) +func (m *Message) ParsePayload(target any) error { if m.Payload == nil { return nil } - result := core.JSONUnmarshal(m.Payload, v) + result := core.JSONUnmarshal(m.Payload, target) if !result.OK { return result.Value.(error) } diff --git a/node/peer.go b/node/peer.go index 60a09c4..2071360 100644 --- a/node/peer.go +++ b/node/peer.go @@ -150,7 +150,7 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { } // Missing files indicate a first run; any existing file must parse cleanly. - if !fsExists(peersPath) { + if !filesystemExists(peersPath) { pr.rebuildKDTree() return pr, nil } @@ -710,7 +710,7 @@ func (r *PeerRegistry) scheduleSave() { func (r *PeerRegistry) saveNow() error { // Ensure directory exists dir := core.PathDir(r.path) - if err := fsEnsureDir(dir); err != nil { + if err := filesystemEnsureDir(dir); err != nil { return core.E("PeerRegistry.saveNow", "failed to create peers directory", err) } @@ -725,12 +725,12 @@ func (r *PeerRegistry) saveNow() error { // Use atomic write pattern: write to temp file, then rename tmpPath := r.path + ".tmp" - if err := fsWrite(tmpPath, string(data)); err != nil { + if err := filesystemWrite(tmpPath, string(data)); err != nil { return core.E("PeerRegistry.saveNow", "failed to write peers temp file", err) } - if err := fsRename(tmpPath, r.path); err != nil { - fsDelete(tmpPath) // Clean up temp file + if err := filesystemRename(tmpPath, r.path); err != nil { + filesystemDelete(tmpPath) // Clean up temp file return core.E("PeerRegistry.saveNow", "failed to rename peers file", err) } @@ -773,7 +773,7 @@ func (r *PeerRegistry) save() error { // load reads peers from disk. func (r *PeerRegistry) load() error { - content, err := fsRead(r.path) + content, err := filesystemRead(r.path) if err != nil { return core.E("PeerRegistry.load", "failed to read peers", err) } diff --git a/node/peer_test.go b/node/peer_test.go index a8ce4d5..d027bad 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -885,7 +885,7 @@ func TestPeer_Registry_SaveNow_Good(t *testing.T) { } // Verify the file was written - if !fsExists(peersPath) { + if !filesystemExists(peersPath) { t.Error("peers.json should exist after saveNow") } } @@ -908,7 +908,7 @@ func TestPeer_Registry_ScheduleSave_TimerFires_Ugly(t *testing.T) { time.Sleep(6 * time.Second) // The file should have been saved by the timer - if !fsExists(peersPath) { + if !filesystemExists(peersPath) { t.Error("peers.json should exist after debounce timer fires") } -- 2.45.3 From 8e640e2d42c635eba12844ae2b850bb637305c0d Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 30 Mar 2026 22:03:34 +0100 Subject: [PATCH 17/60] feat(node): advertise agent identity in transport Co-Authored-By: Virgil --- node/transport.go | 82 ++++++++++++++++++++++++++++++++++++++++-- node/transport_test.go | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/node/transport.go b/node/transport.go index 2cb5aed..2d94b20 100644 --- a/node/transport.go +++ b/node/transport.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "slices" + "strings" "sync" "sync/atomic" "time" @@ -29,6 +30,9 @@ const debugLogInterval = 100 // DefaultMaxMessageSize is the default maximum message size (1MB) const DefaultMaxMessageSize int64 = 1 << 20 // 1MB +// agentUserAgentPrefix identifies this tool in request headers. +const agentUserAgentPrefix = "agent-go-p2p" + // TransportConfig configures the WebSocket transport. // // cfg := DefaultTransportConfig() @@ -181,6 +185,7 @@ type PeerConnection struct { Conn *websocket.Conn SharedSecret []byte // Derived via X25519 ECDH, used for SMSG LastActivity time.Time + UserAgent string // Request identity advertised by the peer writeMu sync.Mutex // Serialize WebSocket writes transport *Transport closeOnce sync.Once // Ensure Close() is only called once @@ -222,6 +227,57 @@ func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportCon } } +// agentHeaderToken converts free-form identity data into a stable header token. +func agentHeaderToken(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "unknown" + } + + var sb strings.Builder + sb.Grow(len(value)) + for _, r := range value { + switch { + case r >= 'a' && r <= 'z': + sb.WriteRune(r) + case r >= 'A' && r <= 'Z': + sb.WriteRune(r) + case r >= '0' && r <= '9': + sb.WriteRune(r) + case r == '-' || r == '_' || r == '.': + sb.WriteRune(r) + case r == ' ': + sb.WriteByte('_') + default: + sb.WriteByte('_') + } + } + + token := sb.String() + if token == "" { + return "unknown" + } + + return token +} + +// agentUserAgent returns a transparent identity string for request headers. +func (t *Transport) agentUserAgent() string { + identity := t.node.Identity() + if identity == nil { + return core.Sprintf("%s proto=%s", agentUserAgentPrefix, ProtocolVersion) + } + + return core.Sprintf( + "%s id=%s name=%s role=%s proto=%s", + agentUserAgentPrefix, + identity.ID, + agentHeaderToken(identity.Name), + identity.Role, + ProtocolVersion, + ) +} + // Start opens the WebSocket listener and background maintenance loops. func (t *Transport) Start() error { mux := http.NewServeMux() @@ -333,12 +389,15 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { scheme = "wss" } peerURL := url.URL{Scheme: scheme, Host: peer.Address, Path: t.config.WSPath} + userAgent := t.agentUserAgent() // Dial the peer with timeout to prevent hanging on unresponsive peers dialer := websocket.Dialer{ HandshakeTimeout: 10 * time.Second, } - conn, _, err := dialer.Dial(peerURL.String(), nil) + conn, _, err := dialer.Dial(peerURL.String(), http.Header{ + "User-Agent": []string{userAgent}, + }) if err != nil { return nil, core.E("Transport.Connect", "failed to connect to peer", err) } @@ -347,6 +406,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { Peer: peer, Conn: conn, LastActivity: time.Now(), + UserAgent: userAgent, transport: t, rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } @@ -364,6 +424,10 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { t.mu.Unlock() logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) + logging.Debug("connected peer metadata", logging.Fields{ + "peer_id": pc.Peer.ID, + "user_agent": pc.UserAgent, + }) // Update registry t.registry.MarkConnected(pc.Peer.ID, true) @@ -442,6 +506,8 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { // handleWSUpgrade handles incoming WebSocket connections. func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { + userAgent := r.Header.Get("User-Agent") + // Enforce MaxConns limit (including pending connections during handshake) t.mu.RLock() currentConns := len(t.conns) @@ -505,6 +571,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "peer_version": payload.Version, "supported_versions": SupportedProtocolVersions, "peer_id": payload.Identity.ID, + "user_agent": userAgent, }) identity := t.node.Identity() if identity != nil { @@ -535,6 +602,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "peer_id": payload.Identity.ID, "peer_name": payload.Identity.Name, "public_key": safeKeyPrefix(payload.Identity.PublicKey), + "user_agent": userAgent, }) // Send rejection before closing identity := t.node.Identity() @@ -577,6 +645,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { Conn: conn, SharedSecret: sharedSecret, LastActivity: time.Now(), + UserAgent: userAgent, transport: t, rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } @@ -626,6 +695,12 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { // Update registry t.registry.MarkConnected(peer.ID, true) + logging.Debug("accepted peer connection", logging.Fields{ + "peer_id": peer.ID, + "peer_name": peer.Name, + "user_agent": userAgent, + }) + // Start read loop t.wg.Add(1) go t.readLoop(pc) @@ -733,8 +808,9 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { } logging.Debug("handshake completed with challenge-response verification", logging.Fields{ - "peer_id": pc.Peer.ID, - "peer_name": pc.Peer.Name, + "peer_id": pc.Peer.ID, + "peer_name": pc.Peer.Name, + "user_agent": pc.UserAgent, }) return nil diff --git a/node/transport_test.go b/node/transport_test.go index 4fe6e1f..56ad9e7 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -4,6 +4,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "sync" "sync/atomic" "testing" @@ -236,6 +237,71 @@ func TestTransport_FullHandshake_Good(t *testing.T) { } } +func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { + serverNM := testNode(t, "ua-server", RoleWorker) + clientNM := testNode(t, "ua-client", RoleController) + serverReg := testRegistry(t) + clientReg := testRegistry(t) + + serverCfg := DefaultTransportConfig() + clientCfg := DefaultTransportConfig() + serverTransport := NewTransport(serverNM, serverReg, serverCfg) + clientTransport := NewTransport(clientNM, clientReg, clientCfg) + + var capturedUserAgent atomic.Value + + mux := http.NewServeMux() + mux.HandleFunc(serverCfg.WSPath, func(w http.ResponseWriter, r *http.Request) { + capturedUserAgent.Store(r.Header.Get("User-Agent")) + serverTransport.handleWSUpgrade(w, r) + }) + + ts := httptest.NewServer(mux) + t.Cleanup(func() { + clientTransport.Stop() + serverTransport.Stop() + ts.Close() + }) + + u, _ := url.Parse(ts.URL) + serverAddr := u.Host + + peer := &Peer{ + ID: serverNM.GetIdentity().ID, + Name: "server", + Address: serverAddr, + Role: RoleWorker, + } + clientReg.AddPeer(peer) + + pc, err := clientTransport.Connect(peer) + if err != nil { + t.Fatalf("client connect failed: %v", err) + } + + ua, ok := capturedUserAgent.Load().(string) + if !ok || ua == "" { + t.Fatal("expected user-agent to be captured during websocket upgrade") + } + if !strings.HasPrefix(ua, agentUserAgentPrefix) { + t.Fatalf("user-agent prefix: got %q, want prefix %q", ua, agentUserAgentPrefix) + } + if !strings.Contains(ua, "id="+clientNM.GetIdentity().ID) { + t.Fatalf("user-agent should include client identity, got %q", ua) + } + if pc.UserAgent != ua { + t.Fatalf("client connection user-agent: got %q, want %q", pc.UserAgent, ua) + } + + serverConn := serverTransport.Connection(clientNM.GetIdentity().ID) + if serverConn == nil { + t.Fatal("server should retain the accepted connection") + } + if serverConn.UserAgent != ua { + t.Fatalf("server connection user-agent: got %q, want %q", serverConn.UserAgent, ua) + } +} + func TestTransport_HandshakeRejectWrongVersion_Bad(t *testing.T) { tp := setupTestTransportPair(t) -- 2.45.3 From 7ce21cdba15b460c59453ca092c1bd235be555a6 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:21:07 +0000 Subject: [PATCH 18/60] refactor(node): adopt AX-native protocol names Co-Authored-By: Virgil --- node/bench_test.go | 8 +- node/controller.go | 22 +++--- node/controller_test.go | 90 +++++++++++----------- node/identity_test.go | 18 ++--- node/integration_test.go | 60 +++++++-------- node/levin/storage.go | 150 ++++++++++++++++++++++++------------- node/levin/storage_test.go | 84 ++++++++++----------- node/message.go | 96 ++++++++++++++++++------ node/message_test.go | 88 +++++++++++----------- node/peer_test.go | 46 ++++++------ node/protocol.go | 10 +-- node/protocol_test.go | 46 ++++++------ node/transport.go | 16 ++-- node/transport_test.go | 80 ++++++++++---------- node/worker.go | 32 ++++---- node/worker_test.go | 130 ++++++++++++++++---------------- 16 files changed, 537 insertions(+), 439 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index 91ef804..f5b0518 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -36,7 +36,7 @@ func BenchmarkDeriveSharedSecret(b *testing.B) { nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) - peerPubKey := nm2.GetIdentity().PublicKey + peerPubKey := nm2.Identity().PublicKey b.ReportAllocs() b.ResetTimer() @@ -73,7 +73,7 @@ func BenchmarkMessageSerialise(b *testing.B) { b.ResetTimer() for b.Loop() { - msg, err := NewMessage(MsgStats, "sender-id", "receiver-id", payload) + msg, err := NewMessage(MessageStats, "sender-id", "receiver-id", payload) if err != nil { b.Fatalf("create message: %v", err) } @@ -98,7 +98,7 @@ func BenchmarkMessageCreateOnly(b *testing.B) { b.ResetTimer() for b.Loop() { - _, err := NewMessage(MsgPing, "sender", "receiver", payload) + _, err := NewMessage(MessagePing, "sender", "receiver", payload) if err != nil { b.Fatalf("create message: %v", err) } @@ -151,7 +151,7 @@ func BenchmarkSMSGEncryptDecrypt(b *testing.B) { nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) - sharedSecret, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) + sharedSecret, _ := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) password := base64.StdEncoding.EncodeToString(sharedSecret) // Prepare a message to encrypt diff --git a/node/controller.go b/node/controller.go index 8fbe40a..4fabe56 100644 --- a/node/controller.go +++ b/node/controller.go @@ -128,7 +128,7 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { return nil, ErrorIdentityNotInitialized } - msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) + msg, err := NewMessage(MessageGetStats, identity.ID, peerID, nil) if err != nil { return nil, core.E("Controller.RemoteStats", "failed to create message", err) } @@ -139,7 +139,7 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { } var stats StatsPayload - if err := ParseResponse(resp, MsgStats, &stats); err != nil { + if err := ParseResponse(resp, MessageStats, &stats); err != nil { return nil, err } @@ -170,7 +170,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi Config: configOverride, } - msg, err := NewMessage(MsgStartMiner, identity.ID, peerID, payload) + msg, err := NewMessage(MessageStartMiner, identity.ID, peerID, payload) if err != nil { return core.E("Controller.StartRemoteMiner", "failed to create message", err) } @@ -181,7 +181,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi } var ack MinerAckPayload - if err := ParseResponse(resp, MsgMinerAck, &ack); err != nil { + if err := ParseResponse(resp, MessageMinerAck, &ack); err != nil { return err } @@ -205,7 +205,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { MinerName: minerName, } - msg, err := NewMessage(MsgStopMiner, identity.ID, peerID, payload) + msg, err := NewMessage(MessageStopMiner, identity.ID, peerID, payload) if err != nil { return core.E("Controller.StopRemoteMiner", "failed to create message", err) } @@ -216,7 +216,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { } var ack MinerAckPayload - if err := ParseResponse(resp, MsgMinerAck, &ack); err != nil { + if err := ParseResponse(resp, MessageMinerAck, &ack); err != nil { return err } @@ -236,12 +236,12 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, return nil, ErrorIdentityNotInitialized } - payload := GetLogsPayload{ + payload := LogsRequestPayload{ MinerName: minerName, Lines: lines, } - msg, err := NewMessage(MsgGetLogs, identity.ID, peerID, payload) + msg, err := NewMessage(MessageGetLogs, identity.ID, peerID, payload) if err != nil { return nil, core.E("Controller.RemoteLogs", "failed to create message", err) } @@ -252,7 +252,7 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, } var logs LogsPayload - if err := ParseResponse(resp, MsgLogs, &logs); err != nil { + if err := ParseResponse(resp, MessageLogs, &logs); err != nil { return nil, err } @@ -314,7 +314,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { SentAt: sentAt.UnixMilli(), } - msg, err := NewMessage(MsgPing, identity.ID, peerID, payload) + msg, err := NewMessage(MessagePing, identity.ID, peerID, payload) if err != nil { return 0, core.E("Controller.PingPeer", "failed to create message", err) } @@ -324,7 +324,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return 0, err } - if err := ValidateResponse(resp, MsgPong); err != nil { + if err := ValidateResponse(resp, MessagePong); err != nil { return 0, err } diff --git a/node/controller_test.go b/node/controller_test.go index 6077cce..075159e 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -40,7 +40,7 @@ func setupControllerPair(t *testing.T) (*Controller, *Worker, *testTransportPair // makeWorkerServer spins up an independent server transport with a Worker // registered, returning the server's NodeManager, address, and a cleanup func. -// Useful for multi-peer tests (GetAllStats, ConcurrentRequests). +// Useful for multi-peer tests (AllStats, ConcurrentRequests). func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { t.Helper() @@ -75,10 +75,10 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { func TestController_RequestResponseCorrelation_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID // Send a ping request via the controller; the server-side worker - // replies with MsgPong, setting ReplyTo to the original message ID. + // replies with MessagePong, setting ReplyTo to the original message ID. rtt, err := controller.PingPeer(serverID) require.NoError(t, err, "PingPeer should succeed") assert.Greater(t, rtt, 0.0, "RTT should be positive") @@ -97,11 +97,11 @@ func TestController_RequestTimeout_Bad(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.GetIdentity().ID - clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.Identity().ID // Use sendRequest directly with a short deadline (PingPeer uses 5s internally). - msg, err := NewMessage(MsgPing, clientID, serverID, PingPayload{ + msg, err := NewMessage(MessagePing, clientID, serverID, PingPayload{ SentAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -126,7 +126,7 @@ func TestController_AutoConnect_Good(t *testing.T) { controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) // Add the server peer to the client registry so auto-connect can resolve it. - serverIdentity := tp.ServerNode.GetIdentity() + serverIdentity := tp.ServerNode.Identity() peer := &Peer{ ID: serverIdentity.ID, Name: "server", @@ -159,7 +159,7 @@ func TestController_GetAllStats_Good(t *testing.T) { for i := range numWorkers { nm, addr, _ := makeWorkerServer(t) - wID := nm.GetIdentity().ID + wID := nm.Identity().ID workerIDs[i] = wID peer := &Peer{ @@ -178,8 +178,8 @@ func TestController_GetAllStats_Good(t *testing.T) { controller := NewController(controllerNM, controllerReg, controllerTransport) - // GetAllStats fetches stats from all connected peers in parallel. - stats := controller.GetAllStats() + // AllStats fetches stats from all connected peers in parallel. + stats := controller.AllStats() assert.Len(t, stats, numWorkers, "should get stats from all connected workers") for _, wID := range workerIDs { @@ -194,10 +194,10 @@ func TestController_GetAllStats_Good(t *testing.T) { func TestController_PingPeerRTT_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID // Record initial peer metrics. - peerBefore := tp.ClientReg.GetPeer(serverID) + peerBefore := tp.ClientReg.Peer(serverID) require.NotNil(t, peerBefore, "server peer should exist in the client registry") initialPingMS := peerBefore.PingMS @@ -208,7 +208,7 @@ func TestController_PingPeerRTT_Good(t *testing.T) { assert.Less(t, rtt, 1000.0, "RTT on loopback should be well under 1000ms") // Verify the peer registry was updated with the measured latency. - peerAfter := tp.ClientReg.GetPeer(serverID) + peerAfter := tp.ClientReg.Peer(serverID) require.NotNil(t, peerAfter, "server peer should still exist after ping") assert.NotEqual(t, initialPingMS, peerAfter.PingMS, "PingMS should be updated after a successful ping") @@ -228,7 +228,7 @@ func TestController_ConcurrentRequests_Ugly(t *testing.T) { for i := range numPeers { nm, addr, _ := makeWorkerServer(t) - pID := nm.GetIdentity().ID + pID := nm.Identity().ID peerIDs[i] = pID peer := &Peer{ @@ -279,11 +279,11 @@ func TestController_DeadPeerCleanup_Good(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.GetIdentity().ID - clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.Identity().ID // Fire off a request that will time out. - msg, err := NewMessage(MsgPing, clientID, serverID, PingPayload{ + msg, err := NewMessage(MessagePing, clientID, serverID, PingPayload{ SentAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -308,7 +308,7 @@ func TestController_DeadPeerCleanup_Good(t *testing.T) { func TestController_MultipleSequentialPings_Good(t *testing.T) { // Ensures sequential requests to the same peer are correctly correlated. controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID for i := range 5 { rtt, err := controller.PingPeer(serverID) @@ -321,7 +321,7 @@ func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { // Multiple goroutines sending requests to the SAME peer simultaneously. // Tests concurrent pending-map insertions/deletions under contention. controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID const goroutines = 10 var wg sync.WaitGroup @@ -343,10 +343,10 @@ func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { func TestController_GetRemoteStats_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - stats, err := controller.GetRemoteStats(serverID) - require.NoError(t, err, "GetRemoteStats should succeed") + stats, err := controller.RemoteStats(serverID) + require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) assert.NotEmpty(t, stats.NodeID, "stats should contain the node ID") @@ -366,7 +366,7 @@ func TestController_ConnectToPeerUnknown_Bad(t *testing.T) { func TestController_DisconnectFromPeer_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection") @@ -387,8 +387,8 @@ func TestController_SendRequestPeerNotFound_Bad(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) - clientID := tp.ClientNode.GetIdentity().ID - msg, err := NewMessage(MsgPing, clientID, "ghost-peer", PingPayload{ + clientID := tp.ClientNode.Identity().ID + msg, err := NewMessage(MessagePing, clientID, "ghost-peer", PingPayload{ SentAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -399,7 +399,7 @@ func TestController_SendRequestPeerNotFound_Bad(t *testing.T) { assert.Contains(t, err.Error(), "peer not found") } -// --- Tests for StartRemoteMiner, StopRemoteMiner, GetRemoteLogs --- +// --- Tests for StartRemoteMiner, StopRemoteMiner, RemoteLogs --- // setupControllerPairWithMiner creates a controller/worker pair where the worker // has a fully configured MinerManager so that start/stop/logs handlers work. @@ -521,7 +521,7 @@ func (m *mockMinerFull) GetConsoleHistory(lines int) []string { func TestController_StartRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID configOverride := RawMessage(`{"pool":"pool.example.com:3333"}`) err := controller.StartRemoteMiner(serverID, "xmrig", "profile-1", configOverride) @@ -530,7 +530,7 @@ func TestController_StartRemoteMiner_Good(t *testing.T) { func TestController_StartRemoteMiner_WithConfig_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID configOverride := RawMessage(`{"pool":"custom-pool:3333","threads":4}`) err := controller.StartRemoteMiner(serverID, "xmrig", "", configOverride) @@ -539,7 +539,7 @@ func TestController_StartRemoteMiner_WithConfig_Good(t *testing.T) { func TestController_StartRemoteMiner_EmptyType_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID err := controller.StartRemoteMiner(serverID, "", "profile-1", nil) require.Error(t, err, "StartRemoteMiner with empty miner type should fail") @@ -563,7 +563,7 @@ func TestController_StartRemoteMiner_NoIdentity_Bad(t *testing.T) { func TestController_StopRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID err := controller.StopRemoteMiner(serverID, "running-miner") require.NoError(t, err, "StopRemoteMiner should succeed for existing miner") @@ -571,7 +571,7 @@ func TestController_StopRemoteMiner_Good(t *testing.T) { func TestController_StopRemoteMiner_NotFound_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID err := controller.StopRemoteMiner(serverID, "non-existent-miner") require.Error(t, err, "StopRemoteMiner should fail for non-existent miner") @@ -592,10 +592,10 @@ func TestController_StopRemoteMiner_NoIdentity_Bad(t *testing.T) { func TestController_GetRemoteLogs_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - lines, err := controller.GetRemoteLogs(serverID, "running-miner", 10) - require.NoError(t, err, "GetRemoteLogs should succeed") + lines, err := controller.RemoteLogs(serverID, "running-miner", 10) + require.NoError(t, err, "RemoteLogs should succeed") require.NotNil(t, lines) assert.Len(t, lines, 3, "should return all 3 console history lines") assert.Contains(t, lines[0], "started") @@ -603,10 +603,10 @@ func TestController_GetRemoteLogs_Good(t *testing.T) { func TestController_GetRemoteLogs_LimitedLines_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - lines, err := controller.GetRemoteLogs(serverID, "running-miner", 1) - require.NoError(t, err, "GetRemoteLogs with limited lines should succeed") + lines, err := controller.RemoteLogs(serverID, "running-miner", 1) + require.NoError(t, err, "RemoteLogs with limited lines should succeed") assert.Len(t, lines, 1, "should return only 1 line") } @@ -618,17 +618,17 @@ func TestController_GetRemoteLogs_NoIdentity_Bad(t *testing.T) { controller := NewController(nmNoID, tp.ClientReg, tp.Client) - _, err = controller.GetRemoteLogs("some-peer", "any-miner", 10) + _, err = controller.RemoteLogs("some-peer", "any-miner", 10) require.Error(t, err) assert.Contains(t, err.Error(), "identity not initialized") } func TestController_GetRemoteStats_WithMiners_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - stats, err := controller.GetRemoteStats(serverID) - require.NoError(t, err, "GetRemoteStats should succeed") + stats, err := controller.RemoteStats(serverID) + require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) assert.NotEmpty(t, stats.NodeID) // The worker has a miner manager with 1 running miner @@ -645,7 +645,7 @@ func TestController_GetRemoteStats_NoIdentity_Bad(t *testing.T) { controller := NewController(nmNoID, tp.ClientReg, tp.Client) - _, err = controller.GetRemoteStats("some-peer") + _, err = controller.RemoteStats("some-peer") require.Error(t, err) assert.Contains(t, err.Error(), "identity not initialized") } @@ -659,7 +659,7 @@ func TestController_ConnectToPeer_Success_Good(t *testing.T) { controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) // Add the server peer to the client registry. - serverIdentity := tp.ServerNode.GetIdentity() + serverIdentity := tp.ServerNode.Identity() peer := &Peer{ ID: serverIdentity.ID, Name: "server", @@ -679,7 +679,7 @@ func TestController_HandleResponse_NonReply_Good(t *testing.T) { controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) // handleResponse should ignore messages without ReplyTo - msg, _ := NewMessage(MsgPing, "sender", "target", PingPayload{SentAt: 123}) + msg, _ := NewMessage(MessagePing, "sender", "target", PingPayload{SentAt: 123}) controller.handleResponse(nil, msg) // No pending entries should be affected @@ -702,7 +702,7 @@ func TestController_HandleResponse_FullChannel_Ugly(t *testing.T) { controller.mu.Unlock() // handleResponse with matching reply should not panic on full channel - msg, _ := NewMessage(MsgPong, "sender", "target", PongPayload{SentAt: 123}) + msg, _ := NewMessage(MessagePong, "sender", "target", PongPayload{SentAt: 123}) msg.ReplyTo = "test-id" controller.handleResponse(nil, msg) diff --git a/node/identity_test.go b/node/identity_test.go index ad8820f..259d316 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -35,7 +35,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Error("node manager should have identity after generation") } - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("identity should not be nil") } @@ -72,8 +72,8 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - originalID := nm1.GetIdentity().ID - originalPubKey := nm1.GetIdentity().PublicKey + originalID := nm1.Identity().ID + originalPubKey := nm1.Identity().PublicKey // Create a new manager - should load existing identity nm2, err := NewNodeManagerFromPaths(keyPath, configPath) @@ -85,7 +85,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Error("second node manager should have loaded existing identity") } - identity := nm2.GetIdentity() + identity := nm2.Identity() if identity.ID != originalID { t.Errorf("expected ID '%s', got '%s'", originalID, identity.ID) } @@ -121,12 +121,12 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { } // Derive shared secrets - should be identical - secret1, err := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) + secret1, err := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) if err != nil { t.Fatalf("failed to derive shared secret from node 1: %v", err) } - secret2, err := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey) + secret2, err := nm2.DeriveSharedSecret(nm1.Identity().PublicKey) if err != nil { t.Fatalf("failed to derive shared secret from node 2: %v", err) } @@ -315,8 +315,8 @@ func TestIdentity_ChallengeResponse_Good(t *testing.T) { } // Both derive the same shared secret - secret1, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) - secret2, _ := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey) + secret1, _ := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) + secret2, _ := nm2.DeriveSharedSecret(nm1.Identity().PublicKey) // Responder signs challenge with their derived secret response := SignChallenge(challenge, secret2) @@ -341,7 +341,7 @@ func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { func TestIdentity_NodeManager_GetIdentity_NilWhenNoIdentity_Bad(t *testing.T) { nm := newTestNodeManagerWithoutIdentity(t) - identity := nm.GetIdentity() + identity := nm.Identity() if identity != nil { t.Error("expected nil identity before generation") } diff --git a/node/integration_test.go b/node/integration_test.go index 46cf730..45116e5 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -34,8 +34,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { controllerNM := newTestNodeManager(t, "integration-controller", RoleController) workerNM := newTestNodeManager(t, "integration-worker", RoleWorker) - controllerIdentity := controllerNM.GetIdentity() - workerIdentity := workerNM.GetIdentity() + controllerIdentity := controllerNM.Identity() + workerIdentity := workerNM.Identity() require.NotNil(t, controllerIdentity, "controller identity should be initialised") require.NotNil(t, workerIdentity, "worker identity should be initialised") assert.NotEmpty(t, controllerIdentity.ID, "controller ID should be non-empty") @@ -122,8 +122,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { "worker should have 1 connected peer") // Verify the peer's real identity is stored. - serverPeerID := workerNM.GetIdentity().ID - conn := controllerTransport.GetConnection(serverPeerID) + serverPeerID := workerNM.Identity().ID + conn := controllerTransport.Connection(serverPeerID) require.NotNil(t, conn, "controller should hold a connection keyed by server's real ID") assert.Equal(t, "integration-worker", conn.Peer.Name) @@ -136,15 +136,15 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { assert.Less(t, rtt, 1000.0, "RTT on loopback should be well under 1s") // Verify registry metrics were updated. - peerAfterPing := controllerReg.GetPeer(serverPeerID) + peerAfterPing := controllerReg.Peer(serverPeerID) require.NotNil(t, peerAfterPing) assert.Greater(t, peerAfterPing.PingMS, 0.0, "PingMS should be updated") // ---------------------------------------------------------------- - // Step 5: Encrypted message exchange — GetRemoteStats + // Step 5: Encrypted message exchange — RemoteStats // ---------------------------------------------------------------- - stats, err := controller.GetRemoteStats(serverPeerID) - require.NoError(t, err, "GetRemoteStats should succeed") + stats, err := controller.RemoteStats(serverPeerID) + require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) assert.Equal(t, workerIdentity.ID, stats.NodeID) assert.Equal(t, "integration-worker", stats.NodeName) @@ -209,7 +209,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // ---------------------------------------------------------------- disconnectReceived := make(chan *Message, 1) workerTransport.OnMessage(func(conn *PeerConnection, msg *Message) { - if msg.Type == MsgDisconnect { + if msg.Type == MessageDisconnect { disconnectReceived <- msg } }) @@ -219,7 +219,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { select { case msg := <-disconnectReceived: - assert.Equal(t, MsgDisconnect, msg.Type) + assert.Equal(t, MessageDisconnect, msg.Type) var payload DisconnectPayload require.NoError(t, msg.ParsePayload(&payload)) assert.Equal(t, "integration test complete", payload.Reason) @@ -242,8 +242,8 @@ func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { nodeA := newTestNodeManager(t, "secret-node-a", RoleDual) nodeB := newTestNodeManager(t, "secret-node-b", RoleDual) - pubKeyA := nodeA.GetIdentity().PublicKey - pubKeyB := nodeB.GetIdentity().PublicKey + pubKeyA := nodeA.Identity().PublicKey + pubKeyB := nodeB.Identity().PublicKey secretFromA, err := nodeA.DeriveSharedSecret(pubKeyB) require.NoError(t, err) @@ -260,7 +260,7 @@ func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { // can send and receive encrypted messages after the handshake. func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID // Controller -> Worker: Ping rtt, err := controller.PingPeer(serverID) @@ -268,7 +268,7 @@ func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { assert.Greater(t, rtt, 0.0) // Controller -> Worker: GetStats - stats, err := controller.GetRemoteStats(serverID) + stats, err := controller.RemoteStats(serverID) require.NoError(t, err) require.NotNil(t, stats) assert.NotEmpty(t, stats.NodeID) @@ -294,7 +294,7 @@ func TestIntegration_MultiPeerTopology_Good(t *testing.T) { for i := range numWorkers { nm, addr, _ := makeWorkerServer(t) - wID := nm.GetIdentity().ID + wID := nm.Identity().ID workerIDs[i] = wID peer := &Peer{ @@ -335,7 +335,7 @@ func TestIntegration_MultiPeerTopology_Good(t *testing.T) { } // Fetch stats from all workers in parallel. - allStats := controller.GetAllStats() + allStats := controller.AllStats() assert.Len(t, allStats, numWorkers, "should get stats from all workers") } @@ -350,7 +350,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { require.NoError(t, err) require.NoError(t, nm1.GenerateIdentity("persistent-node", RoleDual)) - original := nm1.GetIdentity() + original := nm1.Identity() require.NotNil(t, original) // Reload from disk. @@ -358,7 +358,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { require.NoError(t, err) require.True(t, nm2.HasIdentity(), "identity should be loaded from disk") - reloaded := nm2.GetIdentity() + reloaded := nm2.Identity() require.NotNil(t, reloaded) assert.Equal(t, original.ID, reloaded.ID, "ID should persist") @@ -390,7 +390,7 @@ func stmfGenerateKeyPair(dir string) (string, error) { if err := nm.GenerateIdentity("temp-peer", RoleWorker); err != nil { return "", err } - return nm.GetIdentity().PublicKey, nil + return nm.Identity().PublicKey, nil } // TestIntegration_UEPSFullRoundTrip exercises a complete UEPS packet @@ -399,7 +399,7 @@ func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { nodeA := newTestNodeManager(t, "ueps-node-a", RoleController) nodeB := newTestNodeManager(t, "ueps-node-b", RoleWorker) - bPubKey := nodeB.GetIdentity().PublicKey + bPubKey := nodeB.Identity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) require.NoError(t, err, "shared secret derivation should succeed") require.Len(t, sharedSecret, 32, "shared secret should be 32 bytes (SHA-256)") @@ -414,7 +414,7 @@ func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { require.NotEmpty(t, wireData) // Node B derives the same shared secret from A's public key. - aPubKey := nodeA.GetIdentity().PublicKey + aPubKey := nodeA.Identity().PublicKey sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey) require.NoError(t, err) assert.Equal(t, sharedSecret, sharedSecretB, @@ -450,7 +450,7 @@ func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { nodeA := newTestNodeManager(t, "integrity-a", RoleController) nodeB := newTestNodeManager(t, "integrity-b", RoleWorker) - bPubKey := nodeB.GetIdentity().PublicKey + bPubKey := nodeB.Identity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) require.NoError(t, err) @@ -463,7 +463,7 @@ func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { copy(tampered, wireData) tampered[len(tampered)-1] ^= 0xFF - aPubKey := nodeA.GetIdentity().PublicKey + aPubKey := nodeA.Identity().PublicKey sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey) require.NoError(t, err) @@ -500,7 +500,7 @@ func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { t.Cleanup(func() { controllerTransport.Stop() }) peer := &Peer{ - ID: workerNM.GetIdentity().ID, + ID: workerNM.Identity().ID, Name: "worker", Address: u.Host, Role: RoleWorker, @@ -522,7 +522,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { controllerNM := newTestNodeManager(t, "allowed-controller", RoleController) controllerReg := newTestPeerRegistry(t) - workerReg.AllowPublicKey(controllerNM.GetIdentity().PublicKey) + workerReg.AllowPublicKey(controllerNM.Identity().PublicKey) workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig()) worker := NewWorker(workerNM, workerTransport) @@ -542,7 +542,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { t.Cleanup(func() { controllerTransport.Stop() }) peer := &Peer{ - ID: workerNM.GetIdentity().ID, + ID: workerNM.Identity().ID, Name: "worker", Address: u.Host, Role: RoleWorker, @@ -611,7 +611,7 @@ func TestIntegration_MessageSerialiseDeserialise_Good(t *testing.T) { tp := setupTestTransportPair(t) pc := tp.connectClient(t) - original, err := NewMessage(MsgStats, tp.ClientNode.GetIdentity().ID, tp.ServerNode.GetIdentity().ID, StatsPayload{ + original, err := NewMessage(MessageStats, tp.ClientNode.Identity().ID, tp.ServerNode.Identity().ID, StatsPayload{ NodeID: "test-node", NodeName: "test-name", Miners: []MinerStatsItem{ @@ -664,10 +664,10 @@ func TestIntegration_GetRemoteStats_EndToEnd_Good(t *testing.T) { tp.connectClient(t) time.Sleep(100 * time.Millisecond) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - stats, err := controller.GetRemoteStats(serverID) - require.NoError(t, err, "GetRemoteStats should succeed end-to-end") + stats, err := controller.RemoteStats(serverID) + require.NoError(t, err, "RemoteStats should succeed end-to-end") require.NotNil(t, stats) assert.Equal(t, serverID, stats.NodeID) assert.Equal(t, "server", stats.NodeName) diff --git a/node/levin/storage.go b/node/levin/storage.go index a03d303..1e2cfb3 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -69,13 +69,13 @@ var ( // Section is an ordered map of named values forming a portable storage section. // Field iteration order is always alphabetical by key for deterministic encoding. // -// section := Section{"id": StringVal([]byte("peer-1"))} +// section := Section{"id": StringValue([]byte("peer-1"))} type Section map[string]Value // Value holds a typed portable storage value. Use the constructor functions -// (Uint64Val, StringVal, ObjectVal, etc.) to create instances. +// (Uint64Value, StringValue, ObjectValue, etc.) to create instances. // -// value := StringVal([]byte("peer-1")) +// value := StringValue([]byte("peer-1")) type Value struct { Type uint8 @@ -98,98 +98,146 @@ type Value struct { // Scalar constructors // --------------------------------------------------------------------------- -// Uint64Val creates a Value of TypeUint64. +// Uint64Value creates a Value of TypeUint64. // -// value := Uint64Val(42) -func Uint64Val(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } +// value := Uint64Value(42) +func Uint64Value(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } -// Uint32Val creates a Value of TypeUint32. +// Uint32Value creates a Value of TypeUint32. // -// value := Uint32Val(42) -func Uint32Val(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } +// value := Uint32Value(42) +func Uint32Value(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } -// Uint16Val creates a Value of TypeUint16. +// Uint16Value creates a Value of TypeUint16. // -// value := Uint16Val(42) -func Uint16Val(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } +// value := Uint16Value(42) +func Uint16Value(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } -// Uint8Val creates a Value of TypeUint8. +// Uint8Value creates a Value of TypeUint8. // -// value := Uint8Val(42) -func Uint8Val(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } +// value := Uint8Value(42) +func Uint8Value(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } -// Int64Val creates a Value of TypeInt64. +// Int64Value creates a Value of TypeInt64. // -// value := Int64Val(42) -func Int64Val(v int64) Value { return Value{Type: TypeInt64, intVal: v} } +// value := Int64Value(42) +func Int64Value(v int64) Value { return Value{Type: TypeInt64, intVal: v} } -// Int32Val creates a Value of TypeInt32. +// Int32Value creates a Value of TypeInt32. // -// value := Int32Val(42) -func Int32Val(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } +// value := Int32Value(42) +func Int32Value(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } -// Int16Val creates a Value of TypeInt16. +// Int16Value creates a Value of TypeInt16. // -// value := Int16Val(42) -func Int16Val(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } +// value := Int16Value(42) +func Int16Value(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } -// Int8Val creates a Value of TypeInt8. +// Int8Value creates a Value of TypeInt8. // -// value := Int8Val(42) -func Int8Val(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } +// value := Int8Value(42) +func Int8Value(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } -// BoolVal creates a Value of TypeBool. +// BoolValue creates a Value of TypeBool. // -// value := BoolVal(true) -func BoolVal(v bool) Value { return Value{Type: TypeBool, boolVal: v} } +// value := BoolValue(true) +func BoolValue(v bool) Value { return Value{Type: TypeBool, boolVal: v} } -// DoubleVal creates a Value of TypeDouble. +// DoubleValue creates a Value of TypeDouble. // -// value := DoubleVal(3.14) -func DoubleVal(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } +// value := DoubleValue(3.14) +func DoubleValue(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } -// StringVal creates a Value of TypeString. The slice is not copied. +// StringValue creates a Value of TypeString. The slice is not copied. // -// value := StringVal([]byte("hello")) -func StringVal(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } +// value := StringValue([]byte("hello")) +func StringValue(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } -// ObjectVal creates a Value of TypeObject wrapping a nested Section. +// ObjectValue creates a Value of TypeObject wrapping a nested Section. // -// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) -func ObjectVal(s Section) Value { return Value{Type: TypeObject, objectVal: s} } +// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) +func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} } // --------------------------------------------------------------------------- // Array constructors // --------------------------------------------------------------------------- -// Uint64ArrayVal creates a typed array of uint64 values. +// Uint64ArrayValue creates a typed array of uint64 values. // -// value := Uint64ArrayVal([]uint64{1, 2, 3}) -func Uint64ArrayVal(vs []uint64) Value { +// value := Uint64ArrayValue([]uint64{1, 2, 3}) +func Uint64ArrayValue(vs []uint64) Value { return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs} } -// Uint32ArrayVal creates a typed array of uint32 values. +// Uint32ArrayValue creates a typed array of uint32 values. // -// value := Uint32ArrayVal([]uint32{1, 2, 3}) -func Uint32ArrayVal(vs []uint32) Value { +// value := Uint32ArrayValue([]uint32{1, 2, 3}) +func Uint32ArrayValue(vs []uint32) Value { return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs} } -// StringArrayVal creates a typed array of byte-string values. +// StringArrayValue creates a typed array of byte-string values. // -// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) -func StringArrayVal(vs [][]byte) Value { +// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) +func StringArrayValue(vs [][]byte) Value { return Value{Type: ArrayFlag | TypeString, stringArray: vs} } -// ObjectArrayVal creates a typed array of Section values. +// ObjectArrayValue creates a typed array of Section values. // -// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) -func ObjectArrayVal(vs []Section) Value { +// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) +func ObjectArrayValue(vs []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: vs} } +// Deprecated: use Uint64Value. +func Uint64Val(v uint64) Value { return Uint64Value(v) } + +// Deprecated: use Uint32Value. +func Uint32Val(v uint32) Value { return Uint32Value(v) } + +// Deprecated: use Uint16Value. +func Uint16Val(v uint16) Value { return Uint16Value(v) } + +// Deprecated: use Uint8Value. +func Uint8Val(v uint8) Value { return Uint8Value(v) } + +// Deprecated: use Int64Value. +func Int64Val(v int64) Value { return Int64Value(v) } + +// Deprecated: use Int32Value. +func Int32Val(v int32) Value { return Int32Value(v) } + +// Deprecated: use Int16Value. +func Int16Val(v int16) Value { return Int16Value(v) } + +// Deprecated: use Int8Value. +func Int8Val(v int8) Value { return Int8Value(v) } + +// Deprecated: use BoolValue. +func BoolVal(v bool) Value { return BoolValue(v) } + +// Deprecated: use DoubleValue. +func DoubleVal(v float64) Value { return DoubleValue(v) } + +// Deprecated: use StringValue. +func StringVal(v []byte) Value { return StringValue(v) } + +// Deprecated: use ObjectValue. +func ObjectVal(s Section) Value { return ObjectValue(s) } + +// Deprecated: use Uint64ArrayValue. +func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } + +// Deprecated: use Uint32ArrayValue. +func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } + +// Deprecated: use StringArrayValue. +func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } + +// Deprecated: use ObjectArrayValue. +func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } + // --------------------------------------------------------------------------- // Scalar accessors // --------------------------------------------------------------------------- diff --git a/node/levin/storage_test.go b/node/levin/storage_test.go index f16f49e..7118d48 100644 --- a/node/levin/storage_test.go +++ b/node/levin/storage_test.go @@ -37,17 +37,17 @@ func TestStorage_EncodeStorage_EmptySection_Ugly(t *testing.T) { func TestStorage_PrimitivesRoundTrip_Ugly(t *testing.T) { s := Section{ - "u64": Uint64Val(0xDEADBEEFCAFEBABE), - "u32": Uint32Val(0xCAFEBABE), - "u16": Uint16Val(0xBEEF), - "u8": Uint8Val(42), - "i64": Int64Val(-9223372036854775808), - "i32": Int32Val(-2147483648), - "i16": Int16Val(-32768), - "i8": Int8Val(-128), - "flag": BoolVal(true), - "height": StringVal([]byte("hello world")), - "pi": DoubleVal(3.141592653589793), + "u64": Uint64Value(0xDEADBEEFCAFEBABE), + "u32": Uint32Value(0xCAFEBABE), + "u16": Uint16Value(0xBEEF), + "u8": Uint8Value(42), + "i64": Int64Value(-9223372036854775808), + "i32": Int32Value(-2147483648), + "i16": Int16Value(-32768), + "i8": Int8Value(-128), + "flag": BoolValue(true), + "height": StringValue([]byte("hello world")), + "pi": DoubleValue(3.141592653589793), } data, err := EncodeStorage(s) @@ -108,12 +108,12 @@ func TestStorage_PrimitivesRoundTrip_Ugly(t *testing.T) { func TestStorage_NestedObject_Good(t *testing.T) { inner := Section{ - "port": Uint16Val(18080), - "host": StringVal([]byte("127.0.0.1")), + "port": Uint16Value(18080), + "host": StringValue([]byte("127.0.0.1")), } outer := Section{ - "node_data": ObjectVal(inner), - "version": Uint32Val(1), + "node_data": ObjectValue(inner), + "version": Uint32Value(1), } data, err := EncodeStorage(outer) @@ -140,7 +140,7 @@ func TestStorage_NestedObject_Good(t *testing.T) { func TestStorage_Uint64Array_Good(t *testing.T) { s := Section{ - "heights": Uint64ArrayVal([]uint64{10, 20, 30}), + "heights": Uint64ArrayValue([]uint64{10, 20, 30}), } data, err := EncodeStorage(s) @@ -156,7 +156,7 @@ func TestStorage_Uint64Array_Good(t *testing.T) { func TestStorage_StringArray_Good(t *testing.T) { s := Section{ - "peers": StringArrayVal([][]byte{[]byte("foo"), []byte("bar")}), + "peers": StringArrayValue([][]byte{[]byte("foo"), []byte("bar")}), } data, err := EncodeStorage(s) @@ -174,11 +174,11 @@ func TestStorage_StringArray_Good(t *testing.T) { func TestStorage_ObjectArray_Good(t *testing.T) { sections := []Section{ - {"id": Uint32Val(1), "name": StringVal([]byte("alice"))}, - {"id": Uint32Val(2), "name": StringVal([]byte("bob"))}, + {"id": Uint32Value(1), "name": StringValue([]byte("alice"))}, + {"id": Uint32Value(2), "name": StringValue([]byte("bob"))}, } s := Section{ - "nodes": ObjectArrayVal(sections), + "nodes": ObjectArrayValue(sections), } data, err := EncodeStorage(s) @@ -213,25 +213,25 @@ func TestStorage_DecodeStorage_BadSignature_Bad(t *testing.T) { data := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x01, 0x02, 0x01, 0x01, 0x00} _, err := DecodeStorage(data) require.Error(t, err) - assert.ErrorIs(t, err, ErrStorageBadSignature) + assert.ErrorIs(t, err, ErrorStorageBadSignature) } func TestStorage_DecodeStorage_TooShort_Bad(t *testing.T) { _, err := DecodeStorage([]byte{0x01, 0x11}) require.Error(t, err) - assert.ErrorIs(t, err, ErrStorageTruncated) + assert.ErrorIs(t, err, ErrorStorageTruncated) } func TestStorage_ByteIdenticalReencode_Ugly(t *testing.T) { s := Section{ - "alpha": Uint64Val(999), - "bravo": StringVal([]byte("deterministic")), - "charlie": BoolVal(false), - "delta": ObjectVal(Section{ - "x": Int32Val(-42), - "y": Int32Val(100), + "alpha": Uint64Value(999), + "bravo": StringValue([]byte("deterministic")), + "charlie": BoolValue(false), + "delta": ObjectValue(Section{ + "x": Int32Value(-42), + "y": Int32Value(100), }), - "echo": Uint64ArrayVal([]uint64{1, 2, 3}), + "echo": Uint64ArrayValue([]uint64{1, 2, 3}), } data1, err := EncodeStorage(s) @@ -247,27 +247,27 @@ func TestStorage_ByteIdenticalReencode_Ugly(t *testing.T) { } func TestStorage_TypeMismatchErrors_Bad(t *testing.T) { - v := Uint64Val(42) + v := Uint64Value(42) _, err := v.AsUint32() - assert.ErrorIs(t, err, ErrStorageTypeMismatch) + assert.ErrorIs(t, err, ErrorStorageTypeMismatch) _, err = v.AsString() - assert.ErrorIs(t, err, ErrStorageTypeMismatch) + assert.ErrorIs(t, err, ErrorStorageTypeMismatch) _, err = v.AsBool() - assert.ErrorIs(t, err, ErrStorageTypeMismatch) + assert.ErrorIs(t, err, ErrorStorageTypeMismatch) _, err = v.AsSection() - assert.ErrorIs(t, err, ErrStorageTypeMismatch) + assert.ErrorIs(t, err, ErrorStorageTypeMismatch) _, err = v.AsUint64Array() - assert.ErrorIs(t, err, ErrStorageTypeMismatch) + assert.ErrorIs(t, err, ErrorStorageTypeMismatch) } func TestStorage_Uint32Array_Good(t *testing.T) { s := Section{ - "ports": Uint32ArrayVal([]uint32{8080, 8443, 9090}), + "ports": Uint32ArrayValue([]uint32{8080, 8443, 9090}), } data, err := EncodeStorage(s) @@ -286,14 +286,14 @@ func TestStorage_DecodeStorage_BadVersion_Bad(t *testing.T) { data := []byte{0x01, 0x11, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x00} _, err := DecodeStorage(data) require.Error(t, err) - assert.ErrorIs(t, err, ErrStorageBadVersion) + assert.ErrorIs(t, err, ErrorStorageBadVersion) } func TestStorage_EmptyArrays_Ugly(t *testing.T) { s := Section{ - "empty_u64": Uint64ArrayVal([]uint64{}), - "empty_str": StringArrayVal([][]byte{}), - "empty_obj": ObjectArrayVal([]Section{}), + "empty_u64": Uint64ArrayValue([]uint64{}), + "empty_str": StringArrayValue([][]byte{}), + "empty_obj": ObjectArrayValue([]Section{}), } data, err := EncodeStorage(s) @@ -317,8 +317,8 @@ func TestStorage_EmptyArrays_Ugly(t *testing.T) { func TestStorage_BoolFalseRoundTrip_Ugly(t *testing.T) { s := Section{ - "off": BoolVal(false), - "on": BoolVal(true), + "off": BoolValue(false), + "on": BoolValue(true), } data, err := EncodeStorage(s) diff --git a/node/message.go b/node/message.go index 98f474f..c321cc2 100644 --- a/node/message.go +++ b/node/message.go @@ -60,39 +60,86 @@ func IsProtocolVersionSupported(version string) bool { // MessageType defines the type of P2P message. // -// messageType := MsgPing +// messageType := MessagePing type MessageType string const ( // Connection lifecycle - MsgHandshake MessageType = "handshake" - MsgHandshakeAck MessageType = "handshake_ack" - MsgPing MessageType = "ping" - MsgPong MessageType = "pong" - MsgDisconnect MessageType = "disconnect" + MessageHandshake MessageType = "handshake" + MessageHandshakeAck MessageType = "handshake_ack" + MessagePing MessageType = "ping" + MessagePong MessageType = "pong" + MessageDisconnect MessageType = "disconnect" // Miner operations - MsgGetStats MessageType = "get_stats" - MsgStats MessageType = "stats" - MsgStartMiner MessageType = "start_miner" - MsgStopMiner MessageType = "stop_miner" - MsgMinerAck MessageType = "miner_ack" + MessageGetStats MessageType = "get_stats" + MessageStats MessageType = "stats" + MessageStartMiner MessageType = "start_miner" + MessageStopMiner MessageType = "stop_miner" + MessageMinerAck MessageType = "miner_ack" // Deployment - MsgDeploy MessageType = "deploy" - MsgDeployAck MessageType = "deploy_ack" + MessageDeploy MessageType = "deploy" + MessageDeployAck MessageType = "deploy_ack" // Logs - MsgGetLogs MessageType = "get_logs" - MsgLogs MessageType = "logs" + MessageGetLogs MessageType = "get_logs" + MessageLogs MessageType = "logs" // Error response - MsgError MessageType = "error" + MessageError MessageType = "error" +) + +const ( + // Deprecated: use MessageHandshake. + MsgHandshake = MessageHandshake + + // Deprecated: use MessageHandshakeAck. + MsgHandshakeAck = MessageHandshakeAck + + // Deprecated: use MessagePing. + MsgPing = MessagePing + + // Deprecated: use MessagePong. + MsgPong = MessagePong + + // Deprecated: use MessageDisconnect. + MsgDisconnect = MessageDisconnect + + // Deprecated: use MessageGetStats. + MsgGetStats = MessageGetStats + + // Deprecated: use MessageStats. + MsgStats = MessageStats + + // Deprecated: use MessageStartMiner. + MsgStartMiner = MessageStartMiner + + // Deprecated: use MessageStopMiner. + MsgStopMiner = MessageStopMiner + + // Deprecated: use MessageMinerAck. + MsgMinerAck = MessageMinerAck + + // Deprecated: use MessageDeploy. + MsgDeploy = MessageDeploy + + // Deprecated: use MessageDeployAck. + MsgDeployAck = MessageDeployAck + + // Deprecated: use MessageGetLogs. + MsgGetLogs = MessageGetLogs + + // Deprecated: use MessageLogs. + MsgLogs = MessageLogs + + // Deprecated: use MessageError. + MsgError = MessageError ) // Message represents a P2P message between nodes. // -// message, err := NewMessage(MsgPing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) +// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -105,7 +152,7 @@ type Message struct { // NewMessage builds a message with a generated ID and timestamp. // -// message, err := NewMessage(MsgPing, "controller", "worker-1", PingPayload{SentAt: 42}) +// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) func NewMessage(messageType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -128,7 +175,7 @@ func NewMessage(messageType MessageType, from, to string, payload any) (*Message // Reply creates a response message that points back to the original. // -// reply, err := message.Reply(MsgPong, PongPayload{SentAt: 42, ReceivedAt: 43}) +// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) { reply, err := NewMessage(messageType, m.To, m.From, payload) if err != nil { @@ -239,15 +286,18 @@ type StatsPayload struct { Uptime int64 `json:"uptime"` // Node uptime in seconds } -// GetLogsPayload requests console logs from a miner. +// LogsRequestPayload requests console logs from a miner. // -// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} -type GetLogsPayload struct { +// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} +type LogsRequestPayload struct { MinerName string `json:"minerName"` Lines int `json:"lines"` // Number of lines to fetch Since int64 `json:"since,omitempty"` // Unix timestamp, logs after this time } +// Deprecated: use LogsRequestPayload. +type GetLogsPayload = LogsRequestPayload + // LogsPayload contains console log lines. // // payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} @@ -317,7 +367,7 @@ const ( // // msg, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { - msg, err := NewMessage(MsgError, from, to, ErrorPayload{ + msg, err := NewMessage(MessageError, from, to, ErrorPayload{ Code: code, Message: message, }) diff --git a/node/message_test.go b/node/message_test.go index db1d9e3..85ec47d 100644 --- a/node/message_test.go +++ b/node/message_test.go @@ -7,13 +7,13 @@ import ( func TestMessage_NewMessage_Good(t *testing.T) { t.Run("BasicMessage", func(t *testing.T) { - msg, err := NewMessage(MsgPing, "sender-id", "receiver-id", nil) + msg, err := NewMessage(MessagePing, "sender-id", "receiver-id", nil) if err != nil { t.Fatalf("failed to create message: %v", err) } - if msg.Type != MsgPing { - t.Errorf("expected type MsgPing, got %s", msg.Type) + if msg.Type != MessagePing { + t.Errorf("expected type MessagePing, got %s", msg.Type) } if msg.From != "sender-id" { @@ -38,7 +38,7 @@ func TestMessage_NewMessage_Good(t *testing.T) { SentAt: time.Now().UnixMilli(), } - msg, err := NewMessage(MsgPing, "sender", "receiver", payload) + msg, err := NewMessage(MessagePing, "sender", "receiver", payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -60,9 +60,9 @@ func TestMessage_NewMessage_Good(t *testing.T) { } func TestMessage_Reply_Good(t *testing.T) { - original, _ := NewMessage(MsgPing, "sender", "receiver", PingPayload{SentAt: 12345}) + original, _ := NewMessage(MessagePing, "sender", "receiver", PingPayload{SentAt: 12345}) - reply, err := original.Reply(MsgPong, PongPayload{ + reply, err := original.Reply(MessagePong, PongPayload{ SentAt: 12345, ReceivedAt: 12350, }) @@ -83,8 +83,8 @@ func TestMessage_Reply_Good(t *testing.T) { t.Error("reply To should be original From") } - if reply.Type != MsgPong { - t.Errorf("expected type MsgPong, got %s", reply.Type) + if reply.Type != MessagePong { + t.Errorf("expected type MessagePong, got %s", reply.Type) } } @@ -95,7 +95,7 @@ func TestMessage_ParsePayload_Good(t *testing.T) { ProfileID: "test-profile", } - msg, _ := NewMessage(MsgStartMiner, "ctrl", "worker", payload) + msg, _ := NewMessage(MessageStartMiner, "ctrl", "worker", payload) var parsed StartMinerPayload err := msg.ParsePayload(&parsed) @@ -109,7 +109,7 @@ func TestMessage_ParsePayload_Good(t *testing.T) { }) t.Run("NilPayload", func(t *testing.T) { - msg, _ := NewMessage(MsgGetStats, "ctrl", "worker", nil) + msg, _ := NewMessage(MessageGetStats, "ctrl", "worker", nil) var parsed StatsPayload err := msg.ParsePayload(&parsed) @@ -137,7 +137,7 @@ func TestMessage_ParsePayload_Good(t *testing.T) { Uptime: 86400, } - msg, _ := NewMessage(MsgStats, "worker", "ctrl", stats) + msg, _ := NewMessage(MessageStats, "worker", "ctrl", stats) var parsed StatsPayload err := msg.ParsePayload(&parsed) @@ -160,13 +160,13 @@ func TestMessage_ParsePayload_Good(t *testing.T) { } func TestMessage_NewErrorMessage_Bad(t *testing.T) { - errMsg, err := NewErrorMessage("sender", "receiver", ErrCodeOperationFailed, "something went wrong", "original-msg-id") + errMsg, err := NewErrorMessage("sender", "receiver", ErrorCodeOperationFailed, "something went wrong", "original-msg-id") if err != nil { t.Fatalf("failed to create error message: %v", err) } - if errMsg.Type != MsgError { - t.Errorf("expected type MsgError, got %s", errMsg.Type) + if errMsg.Type != MessageError { + t.Errorf("expected type MessageError, got %s", errMsg.Type) } if errMsg.ReplyTo != "original-msg-id" { @@ -179,8 +179,8 @@ func TestMessage_NewErrorMessage_Bad(t *testing.T) { t.Fatalf("failed to parse error payload: %v", err) } - if errPayload.Code != ErrCodeOperationFailed { - t.Errorf("expected code %d, got %d", ErrCodeOperationFailed, errPayload.Code) + if errPayload.Code != ErrorCodeOperationFailed { + t.Errorf("expected code %d, got %d", ErrorCodeOperationFailed, errPayload.Code) } if errPayload.Message != "something went wrong" { @@ -189,7 +189,7 @@ func TestMessage_NewErrorMessage_Bad(t *testing.T) { } func TestMessage_Serialization_Good(t *testing.T) { - original, _ := NewMessage(MsgStartMiner, "ctrl", "worker", StartMinerPayload{ + original, _ := NewMessage(MessageStartMiner, "ctrl", "worker", StartMinerPayload{ MinerType: "xmrig", ProfileID: "my-profile", }) @@ -225,21 +225,21 @@ func TestMessage_Serialization_Good(t *testing.T) { func TestMessage_Types_Good(t *testing.T) { types := []MessageType{ - MsgHandshake, - MsgHandshakeAck, - MsgPing, - MsgPong, - MsgDisconnect, - MsgGetStats, - MsgStats, - MsgStartMiner, - MsgStopMiner, - MsgMinerAck, - MsgDeploy, - MsgDeployAck, - MsgGetLogs, - MsgLogs, - MsgError, + MessageHandshake, + MessageHandshakeAck, + MessagePing, + MessagePong, + MessageDisconnect, + MessageGetStats, + MessageStats, + MessageStartMiner, + MessageStopMiner, + MessageMinerAck, + MessageDeploy, + MessageDeployAck, + MessageGetLogs, + MessageLogs, + MessageError, } for _, msgType := range types { @@ -258,12 +258,12 @@ func TestMessage_Types_Good(t *testing.T) { func TestMessage_ErrorCodes_Bad(t *testing.T) { codes := map[int]string{ - ErrCodeUnknown: "Unknown", - ErrCodeInvalidMessage: "InvalidMessage", - ErrCodeUnauthorized: "Unauthorized", - ErrCodeNotFound: "NotFound", - ErrCodeOperationFailed: "OperationFailed", - ErrCodeTimeout: "Timeout", + ErrorCodeUnknown: "Unknown", + ErrorCodeInvalidMessage: "InvalidMessage", + ErrorCodeUnauthorized: "Unauthorized", + ErrorCodeNotFound: "NotFound", + ErrorCodeOperationFailed: "OperationFailed", + ErrorCodeTimeout: "Timeout", } for code, name := range codes { @@ -276,7 +276,7 @@ func TestMessage_ErrorCodes_Bad(t *testing.T) { } func TestMessage_NewMessage_NilPayload_Ugly(t *testing.T) { - msg, err := NewMessage(MsgPing, "from", "to", nil) + msg, err := NewMessage(MessagePing, "from", "to", nil) if err != nil { t.Fatalf("NewMessage with nil payload should succeed: %v", err) } @@ -295,12 +295,12 @@ func TestMessage_ParsePayload_Nil_Ugly(t *testing.T) { } func TestMessage_NewErrorMessage_Success_Bad(t *testing.T) { - msg, err := NewErrorMessage("from", "to", ErrCodeOperationFailed, "something went wrong", "reply-123") + msg, err := NewErrorMessage("from", "to", ErrorCodeOperationFailed, "something went wrong", "reply-123") if err != nil { t.Fatalf("NewErrorMessage failed: %v", err) } - if msg.Type != MsgError { - t.Errorf("expected type %s, got %s", MsgError, msg.Type) + if msg.Type != MessageError { + t.Errorf("expected type %s, got %s", MessageError, msg.Type) } if msg.ReplyTo != "reply-123" { t.Errorf("expected ReplyTo 'reply-123', got '%s'", msg.ReplyTo) @@ -311,8 +311,8 @@ func TestMessage_NewErrorMessage_Success_Bad(t *testing.T) { if err != nil { t.Fatalf("ParsePayload failed: %v", err) } - if payload.Code != ErrCodeOperationFailed { - t.Errorf("expected code %d, got %d", ErrCodeOperationFailed, payload.Code) + if payload.Code != ErrorCodeOperationFailed { + t.Errorf("expected code %d, got %d", ErrorCodeOperationFailed, payload.Code) } if payload.Message != "something went wrong" { t.Errorf("expected message 'something went wrong', got '%s'", payload.Message) diff --git a/node/peer_test.go b/node/peer_test.go index d027bad..2365b32 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -86,7 +86,7 @@ func TestPeer_Registry_GetPeer_Good(t *testing.T) { pr.AddPeer(peer) - retrieved := pr.GetPeer("get-test-peer") + retrieved := pr.Peer("get-test-peer") if retrieved == nil { t.Fatal("failed to retrieve peer") } @@ -96,7 +96,7 @@ func TestPeer_Registry_GetPeer_Good(t *testing.T) { } // Non-existent peer - nonExistent := pr.GetPeer("non-existent") + nonExistent := pr.Peer("non-existent") if nonExistent != nil { t.Error("expected nil for non-existent peer") } @@ -173,7 +173,7 @@ func TestPeer_Registry_UpdateMetrics_Good(t *testing.T) { t.Fatalf("failed to update metrics: %v", err) } - updated := pr.GetPeer("metrics-test") + updated := pr.Peer("metrics-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -205,7 +205,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated := pr.GetPeer("score-test") + updated := pr.Peer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -219,7 +219,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated = pr.GetPeer("score-test") + updated = pr.Peer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -233,7 +233,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated = pr.GetPeer("score-test") + updated = pr.Peer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -256,7 +256,7 @@ func TestPeer_Registry_SetConnected_Good(t *testing.T) { pr.SetConnected("connect-test", true) - updated := pr.GetPeer("connect-test") + updated := pr.Peer("connect-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -268,7 +268,7 @@ func TestPeer_Registry_SetConnected_Good(t *testing.T) { } pr.SetConnected("connect-test", false) - updated = pr.GetPeer("connect-test") + updated = pr.Peer("connect-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -294,7 +294,7 @@ func TestPeer_Registry_GetConnectedPeers_Good(t *testing.T) { pr.SetConnected("conn-1", true) pr.SetConnected("conn-3", true) - connected := pr.GetConnectedPeers() + connected := pr.ConnectedPeerList() if len(connected) != 2 { t.Errorf("expected 2 connected peers, got %d", len(connected)) } @@ -382,7 +382,7 @@ func TestPeer_Registry_Persistence_Good(t *testing.T) { t.Errorf("expected 1 peer after reload, got %d", pr2.Count()) } - loaded := pr2.GetPeer("persist-test") + loaded := pr2.Peer("persist-test") if loaded == nil { t.Fatal("peer should exist after reload") } @@ -399,20 +399,20 @@ func TestPeer_Registry_AuthMode_Good(t *testing.T) { defer cleanup() // Default should be Open - if pr.GetAuthMode() != PeerAuthOpen { - t.Errorf("expected default auth mode to be Open, got %d", pr.GetAuthMode()) + if pr.AuthMode() != PeerAuthOpen { + t.Errorf("expected default auth mode to be Open, got %d", pr.AuthMode()) } // Set to Allowlist pr.SetAuthMode(PeerAuthAllowlist) - if pr.GetAuthMode() != PeerAuthAllowlist { - t.Errorf("expected auth mode to be Allowlist after setting, got %d", pr.GetAuthMode()) + if pr.AuthMode() != PeerAuthAllowlist { + t.Errorf("expected auth mode to be Allowlist after setting, got %d", pr.AuthMode()) } // Set back to Open pr.SetAuthMode(PeerAuthOpen) - if pr.GetAuthMode() != PeerAuthOpen { - t.Errorf("expected auth mode to be Open after resetting, got %d", pr.GetAuthMode()) + if pr.AuthMode() != PeerAuthOpen { + t.Errorf("expected auth mode to be Open after resetting, got %d", pr.AuthMode()) } } @@ -563,7 +563,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 5 { pr.RecordSuccess("score-record-test") } - updated := pr.GetPeer("score-record-test") + updated := pr.Peer("score-record-test") if updated.Score <= 50 { t.Errorf("score should increase after successes, got %f", updated.Score) } @@ -573,7 +573,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 3 { pr.RecordFailure("score-record-test") } - updated = pr.GetPeer("score-record-test") + updated = pr.Peer("score-record-test") if updated.Score >= initialScore { t.Errorf("score should decrease after failures, got %f (was %f)", updated.Score, initialScore) } @@ -581,7 +581,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { // Record timeouts - score should decrease initialScore = updated.Score pr.RecordTimeout("score-record-test") - updated = pr.GetPeer("score-record-test") + updated = pr.Peer("score-record-test") if updated.Score >= initialScore { t.Errorf("score should decrease after timeout, got %f (was %f)", updated.Score, initialScore) } @@ -590,7 +590,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 100 { pr.RecordSuccess("score-record-test") } - updated = pr.GetPeer("score-record-test") + updated = pr.Peer("score-record-test") if updated.Score > ScoreMaximum { t.Errorf("score should be clamped to max %f, got %f", ScoreMaximum, updated.Score) } @@ -598,7 +598,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 100 { pr.RecordFailure("score-record-test") } - updated = pr.GetPeer("score-record-test") + updated = pr.Peer("score-record-test") if updated.Score < ScoreMinimum { t.Errorf("score should be clamped to min %f, got %f", ScoreMinimum, updated.Score) } @@ -619,7 +619,7 @@ func TestPeer_Registry_GetPeersByScore_Good(t *testing.T) { pr.AddPeer(p) } - sorted := pr.GetPeersByScore() + sorted := pr.PeersSortedByScore() if len(sorted) != 3 { t.Fatalf("expected 3 peers, got %d", len(sorted)) } @@ -726,7 +726,7 @@ func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { t.Fatalf("failed to update peer: %v", err) } - updated := pr.GetPeer("update-test") + updated := pr.Peer("update-test") if updated == nil { t.Fatal("expected peer to exist after update") } diff --git a/node/protocol.go b/node/protocol.go index ed446f0..bcd4a65 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -23,14 +23,14 @@ type ResponseHandler struct{} // ValidateResponse checks a response against the expected type. // -// err := handler.ValidateResponse(resp, MsgStats) +// err := handler.ValidateResponse(resp, MessageStats) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { return core.E("ResponseHandler.ValidateResponse", "nil response", nil) } // Check for error response - if resp.Type == MsgError { + if resp.Type == MessageError { var errPayload ErrorPayload if err := resp.ParsePayload(&errPayload); err != nil { return &ProtocolError{Code: ErrorCodeUnknown, Message: "unable to parse error response"} @@ -48,7 +48,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy // ParseResponse validates the response and parses the payload into the target. // -// err := handler.ParseResponse(resp, MsgStats, &stats) +// err := handler.ParseResponse(resp, MessageStats, &stats) func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error { if err := h.ValidateResponse(resp, expectedType); err != nil { return err @@ -68,14 +68,14 @@ var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. // -// err := ValidateResponse(msg, MsgStats) +// err := ValidateResponse(msg, MessageStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } // ParseResponse is a convenience function using the default handler. // -// err := ParseResponse(msg, MsgStats, &stats) +// err := ParseResponse(msg, MessageStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } diff --git a/node/protocol_test.go b/node/protocol_test.go index 92535c6..2fd3074 100644 --- a/node/protocol_test.go +++ b/node/protocol_test.go @@ -10,15 +10,15 @@ func TestProtocol_ResponseHandler_ValidateResponse_Good(t *testing.T) { handler := &ResponseHandler{} t.Run("NilResponse", func(t *testing.T) { - err := handler.ValidateResponse(nil, MsgStats) + err := handler.ValidateResponse(nil, MessageStats) if err == nil { t.Error("Expected error for nil response") } }) t.Run("ErrorResponse", func(t *testing.T) { - errMsg, _ := NewErrorMessage("sender", "receiver", ErrCodeOperationFailed, "operation failed", "") - err := handler.ValidateResponse(errMsg, MsgStats) + errMsg, _ := NewErrorMessage("sender", "receiver", ErrorCodeOperationFailed, "operation failed", "") + err := handler.ValidateResponse(errMsg, MessageStats) if err == nil { t.Fatal("Expected error for error response") } @@ -27,14 +27,14 @@ func TestProtocol_ResponseHandler_ValidateResponse_Good(t *testing.T) { t.Errorf("Expected ProtocolError, got %T", err) } - if GetProtocolErrorCode(err) != ErrCodeOperationFailed { - t.Errorf("Expected code %d, got %d", ErrCodeOperationFailed, GetProtocolErrorCode(err)) + if ProtocolErrorCode(err) != ErrorCodeOperationFailed { + t.Errorf("Expected code %d, got %d", ErrorCodeOperationFailed, ProtocolErrorCode(err)) } }) t.Run("WrongType", func(t *testing.T) { - msg, _ := NewMessage(MsgPong, "sender", "receiver", nil) - err := handler.ValidateResponse(msg, MsgStats) + msg, _ := NewMessage(MessagePong, "sender", "receiver", nil) + err := handler.ValidateResponse(msg, MessageStats) if err == nil { t.Error("Expected error for wrong type") } @@ -44,8 +44,8 @@ func TestProtocol_ResponseHandler_ValidateResponse_Good(t *testing.T) { }) t.Run("ValidResponse", func(t *testing.T) { - msg, _ := NewMessage(MsgStats, "sender", "receiver", StatsPayload{NodeID: "test"}) - err := handler.ValidateResponse(msg, MsgStats) + msg, _ := NewMessage(MessageStats, "sender", "receiver", StatsPayload{NodeID: "test"}) + err := handler.ValidateResponse(msg, MessageStats) if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -61,10 +61,10 @@ func TestProtocol_ResponseHandler_ParseResponse_Good(t *testing.T) { NodeName: "Test Node", Uptime: 3600, } - msg, _ := NewMessage(MsgStats, "sender", "receiver", payload) + msg, _ := NewMessage(MessageStats, "sender", "receiver", payload) var parsed StatsPayload - err := handler.ParseResponse(msg, MsgStats, &parsed) + err := handler.ParseResponse(msg, MessageStats, &parsed) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -82,10 +82,10 @@ func TestProtocol_ResponseHandler_ParseResponse_Good(t *testing.T) { Success: true, MinerName: "xmrig-1", } - msg, _ := NewMessage(MsgMinerAck, "sender", "receiver", payload) + msg, _ := NewMessage(MessageMinerAck, "sender", "receiver", payload) var parsed MinerAckPayload - err := handler.ParseResponse(msg, MsgMinerAck, &parsed) + err := handler.ParseResponse(msg, MessageMinerAck, &parsed) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -99,10 +99,10 @@ func TestProtocol_ResponseHandler_ParseResponse_Good(t *testing.T) { }) t.Run("ErrorResponse", func(t *testing.T) { - errMsg, _ := NewErrorMessage("sender", "receiver", ErrCodeNotFound, "not found", "") + errMsg, _ := NewErrorMessage("sender", "receiver", ErrorCodeNotFound, "not found", "") var parsed StatsPayload - err := handler.ParseResponse(errMsg, MsgStats, &parsed) + err := handler.ParseResponse(errMsg, MessageStats, &parsed) if err == nil { t.Error("Expected error for error response") } @@ -112,8 +112,8 @@ func TestProtocol_ResponseHandler_ParseResponse_Good(t *testing.T) { }) t.Run("NilTarget", func(t *testing.T) { - msg, _ := NewMessage(MsgPong, "sender", "receiver", nil) - err := handler.ParseResponse(msg, MsgPong, nil) + msg, _ := NewMessage(MessagePong, "sender", "receiver", nil) + err := handler.ParseResponse(msg, MessagePong, nil) if err != nil { t.Errorf("Unexpected error with nil target: %v", err) } @@ -131,22 +131,22 @@ func TestProtocol_Error_Bad(t *testing.T) { t.Error("IsProtocolError should return true") } - if GetProtocolErrorCode(err) != 1001 { - t.Errorf("Expected code 1001, got %d", GetProtocolErrorCode(err)) + if ProtocolErrorCode(err) != 1001 { + t.Errorf("Expected code 1001, got %d", ProtocolErrorCode(err)) } } func TestProtocol_ConvenienceFunctions_Good(t *testing.T) { - msg, _ := NewMessage(MsgStats, "sender", "receiver", StatsPayload{NodeID: "test"}) + msg, _ := NewMessage(MessageStats, "sender", "receiver", StatsPayload{NodeID: "test"}) // Test ValidateResponse - if err := ValidateResponse(msg, MsgStats); err != nil { + if err := ValidateResponse(msg, MessageStats); err != nil { t.Errorf("ValidateResponse failed: %v", err) } // Test ParseResponse var parsed StatsPayload - if err := ParseResponse(msg, MsgStats, &parsed); err != nil { + if err := ParseResponse(msg, MessageStats, &parsed); err != nil { t.Errorf("ParseResponse failed: %v", err) } if parsed.NodeID != "test" { @@ -156,7 +156,7 @@ func TestProtocol_ConvenienceFunctions_Good(t *testing.T) { func TestProtocol_GetProtocolErrorCode_NonProtocolError_Bad(t *testing.T) { err := core.NewError("regular error") - if GetProtocolErrorCode(err) != 0 { + if ProtocolErrorCode(err) != 0 { t.Error("Expected 0 for non-ProtocolError") } } diff --git a/node/transport.go b/node/transport.go index 2d94b20..99660ce 100644 --- a/node/transport.go +++ b/node/transport.go @@ -554,7 +554,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { return } - if msg.Type != MsgHandshake { + if msg.Type != MessageHandshake { conn.Close() return } @@ -580,7 +580,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { Accepted: false, Reason: core.Sprintf("incompatible protocol version %s, supported: %v", payload.Version, SupportedProtocolVersions), } - rejectMsg, _ := NewMessage(MsgHandshakeAck, identity.ID, payload.Identity.ID, rejectPayload) + rejectMsg, _ := NewMessage(MessageHandshakeAck, identity.ID, payload.Identity.ID, rejectPayload) if rejectData, err := MarshalJSON(rejectMsg); err == nil { conn.WriteMessage(websocket.TextMessage, rejectData) } @@ -612,7 +612,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { Accepted: false, Reason: "peer not authorized", } - rejectMsg, _ := NewMessage(MsgHandshakeAck, identity.ID, payload.Identity.ID, rejectPayload) + rejectMsg, _ := NewMessage(MessageHandshakeAck, identity.ID, payload.Identity.ID, rejectPayload) if rejectData, err := MarshalJSON(rejectMsg); err == nil { conn.WriteMessage(websocket.TextMessage, rejectData) } @@ -669,7 +669,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { Accepted: true, } - ackMsg, err := NewMessage(MsgHandshakeAck, identity.ID, peer.ID, ackPayload) + ackMsg, err := NewMessage(MessageHandshakeAck, identity.ID, peer.ID, ackPayload) if err != nil { conn.Close() return @@ -739,7 +739,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { Version: ProtocolVersion, } - msg, err := NewMessage(MsgHandshake, identity.ID, pc.Peer.ID, payload) + msg, err := NewMessage(MessageHandshake, identity.ID, pc.Peer.ID, payload) if err != nil { return core.E("Transport.performHandshake", "create handshake message", err) } @@ -765,7 +765,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return core.E("Transport.performHandshake", "unmarshal handshake ack", result.Value.(error)) } - if ackMsg.Type != MsgHandshakeAck { + if ackMsg.Type != MessageHandshakeAck { return core.E("Transport.performHandshake", "expected handshake_ack, got "+string(ackMsg.Type), nil) } @@ -905,7 +905,7 @@ func (t *Transport) keepalive(pc *PeerConnection) { // Send ping identity := t.node.Identity() - pingMsg, err := NewMessage(MsgPing, identity.ID, pc.Peer.ID, PingPayload{ + pingMsg, err := NewMessage(MessagePing, identity.ID, pc.Peer.ID, PingPayload{ SentAt: time.Now().UnixMilli(), }) if err != nil { @@ -991,7 +991,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { Reason: reason, Code: code, } - msg, msgErr := NewMessage(MsgDisconnect, identity.ID, pc.Peer.ID, payload) + msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) if msgErr == nil { pc.Send(msg) } diff --git a/node/transport_test.go b/node/transport_test.go index 56ad9e7..d47f7d9 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -102,7 +102,7 @@ func (tp *testTransportPair) connectClient(t *testing.T) *PeerConnection { t.Helper() peer := &Peer{ - ID: tp.ServerNode.GetIdentity().ID, + ID: tp.ServerNode.Identity().ID, Name: "server", Address: tp.ServerAddr, Role: RoleWorker, @@ -227,8 +227,8 @@ func TestTransport_FullHandshake_Good(t *testing.T) { } // Verify peer identity was exchanged correctly - serverID := tp.ServerNode.GetIdentity().ID - serverConn := tp.Client.GetConnection(serverID) + serverID := tp.ServerNode.Identity().ID + serverConn := tp.Client.Connection(serverID) if serverConn == nil { t.Fatal("client should have connection to server by server ID") } @@ -238,10 +238,10 @@ func TestTransport_FullHandshake_Good(t *testing.T) { } func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { - serverNM := testNode(t, "ua-server", RoleWorker) - clientNM := testNode(t, "ua-client", RoleController) - serverReg := testRegistry(t) - clientReg := testRegistry(t) + serverNM := newTestNodeManager(t, "ua-server", RoleWorker) + clientNM := newTestNodeManager(t, "ua-client", RoleController) + serverReg := newTestPeerRegistry(t) + clientReg := newTestPeerRegistry(t) serverCfg := DefaultTransportConfig() clientCfg := DefaultTransportConfig() @@ -267,7 +267,7 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { serverAddr := u.Host peer := &Peer{ - ID: serverNM.GetIdentity().ID, + ID: serverNM.Identity().ID, Name: "server", Address: serverAddr, Role: RoleWorker, @@ -286,14 +286,14 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { if !strings.HasPrefix(ua, agentUserAgentPrefix) { t.Fatalf("user-agent prefix: got %q, want prefix %q", ua, agentUserAgentPrefix) } - if !strings.Contains(ua, "id="+clientNM.GetIdentity().ID) { + if !strings.Contains(ua, "id="+clientNM.Identity().ID) { t.Fatalf("user-agent should include client identity, got %q", ua) } if pc.UserAgent != ua { t.Fatalf("client connection user-agent: got %q, want %q", pc.UserAgent, ua) } - serverConn := serverTransport.Connection(clientNM.GetIdentity().ID) + serverConn := serverTransport.Connection(clientNM.Identity().ID) if serverConn == nil { t.Fatal("server should retain the accepted connection") } @@ -313,12 +313,12 @@ func TestTransport_HandshakeRejectWrongVersion_Bad(t *testing.T) { } defer conn.Close() - clientIdentity := tp.ClientNode.GetIdentity() + clientIdentity := tp.ClientNode.Identity() payload := HandshakePayload{ Identity: *clientIdentity, Version: "99.99", // Unsupported } - msg, _ := NewMessage(MsgHandshake, clientIdentity.ID, "", payload) + msg, _ := NewMessage(MessageHandshake, clientIdentity.ID, "", payload) data, _ := MarshalJSON(msg) if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { @@ -351,7 +351,7 @@ func TestTransport_HandshakeRejectAllowlist_Bad(t *testing.T) { tp.ServerReg.SetAuthMode(PeerAuthAllowlist) peer := &Peer{ - ID: tp.ServerNode.GetIdentity().ID, + ID: tp.ServerNode.Identity().ID, Name: "server", Address: tp.ServerAddr, Role: RoleWorker, @@ -378,9 +378,9 @@ func TestTransport_EncryptedMessageRoundTrip_Ugly(t *testing.T) { pc := tp.connectClient(t) // Send an encrypted message from client to server - clientID := tp.ClientNode.GetIdentity().ID - serverID := tp.ServerNode.GetIdentity().ID - sentMsg, _ := NewMessage(MsgPing, clientID, serverID, PingPayload{ + clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.Identity().ID + sentMsg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -390,8 +390,8 @@ func TestTransport_EncryptedMessageRoundTrip_Ugly(t *testing.T) { select { case msg := <-received: - if msg.Type != MsgPing { - t.Errorf("type: got %s, want %s", msg.Type, MsgPing) + if msg.Type != MessagePing { + t.Errorf("type: got %s, want %s", msg.Type, MessagePing) } if msg.ID != sentMsg.ID { t.Error("message ID mismatch after encrypt/decrypt round-trip") @@ -420,9 +420,9 @@ func TestTransport_MessageDedup_Good(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.GetIdentity().ID - serverID := tp.ServerNode.GetIdentity().ID - msg, _ := NewMessage(MsgPing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) + clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.Identity().ID + msg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) // Send the same message twice if err := pc.Send(msg); err != nil { @@ -450,12 +450,12 @@ func TestTransport_RateLimiting_Good(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.GetIdentity().ID - serverID := tp.ServerNode.GetIdentity().ID + clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.Identity().ID // Send 150 messages rapidly (rate limiter burst = 100) for range 150 { - msg, _ := NewMessage(MsgPing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) + msg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) pc.Send(msg) } @@ -498,7 +498,7 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { client1Transport := NewTransport(client1NM, client1Reg, DefaultTransportConfig()) t.Cleanup(func() { client1Transport.Stop() }) - peer1 := &Peer{ID: serverNM.GetIdentity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} + peer1 := &Peer{ID: serverNM.Identity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} client1Reg.AddPeer(peer1) _, err := client1Transport.Connect(peer1) @@ -515,7 +515,7 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { client2Transport := NewTransport(client2NM, client2Reg, DefaultTransportConfig()) t.Cleanup(func() { client2Transport.Stop() }) - peer2 := &Peer{ID: serverNM.GetIdentity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} + peer2 := &Peer{ID: serverNM.Identity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} client2Reg.AddPeer(peer2) _, err = client2Transport.Connect(peer2) @@ -545,9 +545,9 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { // Close the underlying WebSocket on the client side to simulate network failure. // The server's readLoop will detect the broken connection and clean up. - clientID := tp.ClientNode.GetIdentity().ID - serverPeerID := tp.ServerNode.GetIdentity().ID - clientConn := tp.Client.GetConnection(serverPeerID) + clientID := tp.ClientNode.Identity().ID + serverPeerID := tp.ServerNode.Identity().ID + clientConn := tp.Client.Connection(serverPeerID) if clientConn == nil { t.Fatal("client should have connection to server") } @@ -562,7 +562,7 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { default: if tp.Server.ConnectedPeers() == 0 { // Verify registry updated - peer := tp.ServerReg.GetPeer(clientID) + peer := tp.ServerReg.Peer(clientID) if peer != nil && peer.Connected { t.Error("registry should show peer as disconnected") } @@ -586,13 +586,13 @@ func TestTransport_GracefulClose_Ugly(t *testing.T) { // Allow connection to fully establish time.Sleep(50 * time.Millisecond) - // Graceful close should send a MsgDisconnect before closing + // Graceful close should send a MessageDisconnect before closing pc.GracefulClose("test shutdown", DisconnectNormal) // Check if disconnect message was received select { case msg := <-received: - if msg.Type != MsgDisconnect { + if msg.Type != MessageDisconnect { t.Errorf("expected disconnect message, got %s", msg.Type) } var payload DisconnectPayload @@ -618,8 +618,8 @@ func TestTransport_ConcurrentSends_Ugly(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.GetIdentity().ID - serverID := tp.ServerNode.GetIdentity().ID + clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.Identity().ID // Spawn 10 goroutines each sending 5 messages concurrently const goroutines = 10 @@ -629,7 +629,7 @@ func TestTransport_ConcurrentSends_Ugly(t *testing.T) { for range goroutines { wg.Go(func() { for range msgsPerGoroutine { - msg, _ := NewMessage(MsgPing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) + msg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) pc.Send(msg) } }) @@ -667,7 +667,7 @@ func TestTransport_Broadcast_Good(t *testing.T) { counter.Add(1) }) - wID := nm.GetIdentity().ID + wID := nm.Identity().ID peer := &Peer{ ID: wID, Name: "worker", @@ -685,8 +685,8 @@ func TestTransport_Broadcast_Good(t *testing.T) { time.Sleep(100 * time.Millisecond) // Broadcast a message from the controller - controllerID := controllerNM.GetIdentity().ID - msg, _ := NewMessage(MsgPing, controllerID, "", PingPayload{ + controllerID := controllerNM.Identity().ID + msg, _ := NewMessage(MessagePing, controllerID, "", PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -721,8 +721,8 @@ func TestTransport_BroadcastExcludesSender_Good(t *testing.T) { // The server has a connection to the client, but msg.From matches the client's // connection peer ID check, not the server's own ID. Let's verify sender exclusion // by broadcasting from the server with its own ID. - serverID := tp.ServerNode.GetIdentity().ID - msg, _ := NewMessage(MsgPing, serverID, "", PingPayload{SentAt: time.Now().UnixMilli()}) + serverID := tp.ServerNode.Identity().ID + msg, _ := NewMessage(MessagePing, serverID, "", PingPayload{SentAt: time.Now().UnixMilli()}) // This broadcasts from server to all connected peers (the client). // The server itself won't receive it back because it's not connected to itself. diff --git a/node/worker.go b/node/worker.go index 13a5557..884296f 100644 --- a/node/worker.go +++ b/node/worker.go @@ -85,17 +85,17 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { var err error switch msg.Type { - case MsgPing: + case MessagePing: response, err = w.handlePing(msg) - case MsgGetStats: + case MessageGetStats: response, err = w.handleGetStats(msg) - case MsgStartMiner: + case MessageStartMiner: response, err = w.handleStartMiner(msg) - case MsgStopMiner: + case MessageStopMiner: response, err = w.handleStopMiner(msg) - case MsgGetLogs: + case MessageGetLogs: response, err = w.handleGetLogs(msg) - case MsgDeploy: + case MessageDeploy: response, err = w.handleDeploy(conn, msg) default: // Unknown message type - ignore or send error @@ -140,7 +140,7 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { ReceivedAt: time.Now().UnixMilli(), } - return msg.Reply(MsgPong, pong) + return msg.Reply(MessagePong, pong) } // handleGetStats responds with current miner statistics. @@ -172,7 +172,7 @@ func (w *Worker) handleGetStats(msg *Message) (*Message, error) { } } - return msg.Reply(MsgStats, stats) + return msg.Reply(MessageStats, stats) } // convertMinerStats converts miner stats to the protocol format. @@ -244,14 +244,14 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { Success: false, Error: err.Error(), } - return msg.Reply(MsgMinerAck, ack) + return msg.Reply(MessageMinerAck, ack) } ack := MinerAckPayload{ Success: true, MinerName: miner.GetName(), } - return msg.Reply(MsgMinerAck, ack) + return msg.Reply(MessageMinerAck, ack) } // handleStopMiner stops a running miner. @@ -274,7 +274,7 @@ func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { ack.Error = err.Error() } - return msg.Reply(MsgMinerAck, ack) + return msg.Reply(MessageMinerAck, ack) } // handleGetLogs returns console logs from a miner. @@ -283,7 +283,7 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { return nil, ErrorMinerManagerNotConfigured } - var payload GetLogsPayload + var payload LogsRequestPayload if err := msg.ParsePayload(&payload); err != nil { return nil, core.E("Worker.handleGetLogs", "invalid get logs payload", err) } @@ -307,7 +307,7 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { HasMore: len(lines) >= payload.Lines, } - return msg.Reply(MsgLogs, logs) + return msg.Reply(MessageLogs, logs) } // handleDeploy handles deployment of profiles or miner bundles. @@ -355,14 +355,14 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err Name: payload.Name, Error: err.Error(), } - return msg.Reply(MsgDeployAck, ack) + return msg.Reply(MessageDeployAck, ack) } ack := DeployAckPayload{ Success: true, Name: payload.Name, } - return msg.Reply(MsgDeployAck, ack) + return msg.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: // Determine installation directory @@ -406,7 +406,7 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err "miner_path": minerPath, }) - return msg.Reply(MsgDeployAck, ack) + return msg.Reply(MessageDeployAck, ack) default: return nil, core.E("Worker.handleDeploy", "unknown bundle type: "+payload.BundleType, nil) diff --git a/node/worker_test.go b/node/worker_test.go index b29e2a8..af2e921 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -131,12 +131,12 @@ func TestWorker_HandlePing_Good(t *testing.T) { worker.DataDir = t.TempDir() // Create a ping message - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } pingPayload := PingPayload{SentAt: time.Now().UnixMilli()} - pingMsg, err := NewMessage(MsgPing, "sender-id", identity.ID, pingPayload) + pingMsg, err := NewMessage(MessagePing, "sender-id", identity.ID, pingPayload) if err != nil { t.Fatalf("failed to create ping message: %v", err) } @@ -151,8 +151,8 @@ func TestWorker_HandlePing_Good(t *testing.T) { t.Fatal("handlePing returned nil response") } - if response.Type != MsgPong { - t.Errorf("expected response type %s, got %s", MsgPong, response.Type) + if response.Type != MessagePong { + t.Errorf("expected response type %s, got %s", MessagePong, response.Type) } var pong PongPayload @@ -192,11 +192,11 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { worker.DataDir = t.TempDir() // Create a get_stats message - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } - msg, err := NewMessage(MsgGetStats, "sender-id", identity.ID, nil) + msg, err := NewMessage(MessageGetStats, "sender-id", identity.ID, nil) if err != nil { t.Fatalf("failed to create get_stats message: %v", err) } @@ -211,8 +211,8 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { t.Fatal("handleGetStats returned nil response") } - if response.Type != MsgStats { - t.Errorf("expected response type %s, got %s", MsgStats, response.Type) + if response.Type != MessageStats { + t.Errorf("expected response type %s, got %s", MessageStats, response.Type) } var stats StatsPayload @@ -252,12 +252,12 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { worker.DataDir = t.TempDir() // Create a start_miner message - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } payload := StartMinerPayload{MinerType: "xmrig", ProfileID: "test-profile"} - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create start_miner message: %v", err) } @@ -292,12 +292,12 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { worker.DataDir = t.TempDir() // Create a stop_miner message - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } payload := StopMinerPayload{MinerName: "test-miner"} - msg, err := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create stop_miner message: %v", err) } @@ -332,12 +332,12 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { worker.DataDir = t.TempDir() // Create a get_logs message - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } - payload := GetLogsPayload{MinerName: "test-miner", Lines: 100} - msg, err := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) + payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100} + msg, err := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create get_logs message: %v", err) } @@ -372,7 +372,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { worker.DataDir = t.TempDir() // Create a deploy message for profile - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -381,7 +381,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { Data: []byte(`{"id": "test", "name": "Test Profile"}`), Name: "test-profile", } - msg, err := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create deploy message: %v", err) } @@ -416,7 +416,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { worker.DataDir = t.TempDir() // Create a deploy message with unknown type - identity := nm.GetIdentity() + identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -425,7 +425,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { Data: []byte(`{}`), Name: "test", } - msg, err := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create deploy message: %v", err) } @@ -605,14 +605,14 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { mmFull := &mockMinerManagerWithStart{} worker.SetMinerManager(mmFull) - identity := nm.GetIdentity() + identity := nm.Identity() t.Run("WithConfigOverride", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", Config: RawMessage(`{"pool":"test:3333"}`), } - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -622,8 +622,8 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { t.Fatalf("handleStartMiner returned error: %v", err) } - if response.Type != MsgMinerAck { - t.Errorf("expected type %s, got %s", MsgMinerAck, response.Type) + if response.Type != MessageMinerAck { + t.Errorf("expected type %s, got %s", MessageMinerAck, response.Type) } var ack MinerAckPayload @@ -643,7 +643,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { MinerType: "", Config: RawMessage(`{}`), } - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -666,7 +666,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { MinerType: "xmrig", ProfileID: "test-profile", } - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -691,7 +691,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { MinerType: "xmrig", ProfileID: "missing-profile", } - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -710,7 +710,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { MinerType: "xmrig", Config: RawMessage(`{}`), } - msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) + msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload) if err != nil { t.Fatalf("failed to create message: %v", err) } @@ -764,13 +764,13 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() t.Run("Success", func(t *testing.T) { worker.SetMinerManager(&mockMinerManager{}) payload := StopMinerPayload{MinerName: "test-miner"} - msg, _ := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload) response, err := worker.handleStopMiner(msg) if err != nil { @@ -791,7 +791,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { worker.SetMinerManager(&mockMinerManagerFailing{}) payload := StopMinerPayload{MinerName: "missing-miner"} - msg, _ := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload) response, err := worker.handleStopMiner(msg) if err != nil { @@ -828,7 +828,7 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() t.Run("Success", func(t *testing.T) { mm := &mockMinerManager{ @@ -841,16 +841,16 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { } worker.SetMinerManager(mm) - payload := GetLogsPayload{MinerName: "test-miner", Lines: 100} - msg, _ := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) + payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100} + msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) response, err := worker.handleGetLogs(msg) if err != nil { t.Fatalf("handleGetLogs returned error: %v", err) } - if response.Type != MsgLogs { - t.Errorf("expected type %s, got %s", MsgLogs, response.Type) + if response.Type != MessageLogs { + t.Errorf("expected type %s, got %s", MessageLogs, response.Type) } var logs LogsPayload @@ -865,8 +865,8 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { mm := &mockMinerManagerFailing{} worker.SetMinerManager(mm) - payload := GetLogsPayload{MinerName: "non-existent", Lines: 50} - msg, _ := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) + payload := LogsRequestPayload{MinerName: "non-existent", Lines: 50} + msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) _, err := worker.handleGetLogs(msg) if err == nil { @@ -882,16 +882,16 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { } worker.SetMinerManager(mm) - payload := GetLogsPayload{MinerName: "test-miner", Lines: -1} - msg, _ := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) + payload := LogsRequestPayload{MinerName: "test-miner", Lines: -1} + msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) response, err := worker.handleGetLogs(msg) if err != nil { t.Fatalf("handleGetLogs returned error: %v", err) } // Lines <= 0 should be clamped to maxLogLines - if response.Type != MsgLogs { - t.Errorf("expected %s, got %s", MsgLogs, response.Type) + if response.Type != MessageLogs { + t.Errorf("expected %s, got %s", MessageLogs, response.Type) } }) @@ -903,15 +903,15 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { } worker.SetMinerManager(mm) - payload := GetLogsPayload{MinerName: "test-miner", Lines: 999999} - msg, _ := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) + payload := LogsRequestPayload{MinerName: "test-miner", Lines: 999999} + msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) response, err := worker.handleGetLogs(msg) if err != nil { t.Fatalf("handleGetLogs returned error: %v", err) } - if response.Type != MsgLogs { - t.Errorf("expected %s, got %s", MsgLogs, response.Type) + if response.Type != MessageLogs { + t.Errorf("expected %s, got %s", MessageLogs, response.Type) } }) } @@ -935,7 +935,7 @@ func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() // Set miner manager with miners that have real stats mm := &mockMinerManager{ @@ -963,7 +963,7 @@ func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { } worker.SetMinerManager(mm) - msg, _ := NewMessage(MsgGetStats, "sender-id", identity.ID, nil) + msg, _ := NewMessage(MessageGetStats, "sender-id", identity.ID, nil) response, err := worker.handleGetStats(msg) if err != nil { t.Fatalf("handleGetStats returned error: %v", err) @@ -997,7 +997,7 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() msg, _ := NewMessage("unknown_type", "sender-id", identity.ID, nil) // HandleMessage with unknown type should return silently (no panic) @@ -1027,7 +1027,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) - identity := nm.GetIdentity() + identity := nm.Identity() // Create an unencrypted profile bundle for deploy profileJSON := []byte(`{"id": "deploy-test", "name": "Test Profile"}`) @@ -1042,7 +1042,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { Checksum: bundle.Checksum, Name: "deploy-test", } - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) response, err := worker.handleDeploy(nil, msg) if err != nil { @@ -1080,7 +1080,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { worker.DataDir = t.TempDir() worker.SetProfileManager(&mockProfileManagerFailing{}) - identity := nm.GetIdentity() + identity := nm.Identity() profileJSON := []byte(`{"id": "fail-test"}`) bundle, _ := CreateProfileBundleUnencrypted(profileJSON, "fail-test") @@ -1091,7 +1091,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { Checksum: bundle.Checksum, Name: "fail-test", } - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) response, err := worker.handleDeploy(nil, msg) if err != nil { @@ -1127,7 +1127,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) - identity := nm.GetIdentity() + identity := nm.Identity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1151,7 +1151,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { Checksum: bundle.Checksum, Name: "deploy-miner", } - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) conn := &PeerConnection{ SharedSecret: sharedSecret, @@ -1189,7 +1189,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1209,7 +1209,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { Checksum: bundle.Checksum, Name: "full-deploy", } - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) conn := &PeerConnection{SharedSecret: sharedSecret} @@ -1248,7 +1248,7 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { // Set a failing profile manager to exercise the warn-and-continue path worker.SetProfileManager(&mockProfileManagerFailing{}) - identity := nm.GetIdentity() + identity := nm.Identity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1269,7 +1269,7 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { Checksum: bundle.Checksum, Name: "deploy-with-profile", } - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, payload) + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload) conn := &PeerConnection{SharedSecret: sharedSecret} @@ -1297,10 +1297,10 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - identity := nm.GetIdentity() + identity := nm.Identity() // Create a message with invalid payload - msg, _ := NewMessage(MsgDeploy, "sender-id", identity.ID, "invalid-payload-not-struct") + msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, "invalid-payload-not-struct") _, err := worker.handleDeploy(nil, msg) if err == nil { @@ -1323,7 +1323,7 @@ func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - msg, _ := NewMessage(MsgGetStats, "sender-id", "target-id", nil) + msg, _ := NewMessage(MessageGetStats, "sender-id", "target-id", nil) _, err := worker.handleGetStats(msg) if err == nil { t.Error("expected error when identity is not initialized") @@ -1342,7 +1342,7 @@ func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID // Send start_miner which will fail because no manager is set. // The worker should send an error response via the connection. @@ -1381,11 +1381,11 @@ func TestWorker_HandleMessage_GetStats_IntegrationViaWebSocket_Good(t *testing.T tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.GetIdentity().ID + serverID := tp.ServerNode.Identity().ID - stats, err := controller.GetRemoteStats(serverID) + stats, err := controller.RemoteStats(serverID) if err != nil { - t.Fatalf("GetRemoteStats failed: %v", err) + t.Fatalf("RemoteStats failed: %v", err) } if len(stats.Miners) != 1 { t.Errorf("expected 1 miner, got %d", len(stats.Miners)) -- 2.45.3 From c03b3410e656ed6fccc70fa44095bee6e6d08cdf Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:27:45 +0000 Subject: [PATCH 19/60] refactor(node): remove legacy compatibility aliases Co-Authored-By: Virgil --- logging/logger.go | 15 -------- node/controller.go | 15 -------- node/controller_test.go | 22 ++++++------ node/dispatcher.go | 9 ----- node/dispatcher_test.go | 14 ++++---- node/errors.go | 6 ---- node/identity.go | 10 ------ node/integration_test.go | 14 ++++---- node/levin/connection_test.go | 4 +-- node/levin/header.go | 15 -------- node/levin/header_test.go | 10 +++--- node/levin/storage.go | 66 ---------------------------------- node/levin/varint.go | 6 ---- node/levin/varint_test.go | 8 ++--- node/message.go | 68 ----------------------------------- node/peer.go | 30 ---------------- node/peer_test.go | 20 +++++------ node/protocol.go | 5 --- node/transport.go | 10 ------ node/transport_test.go | 26 +++++++------- node/worker.go | 5 --- 21 files changed, 59 insertions(+), 319 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index fccdae2..a24c227 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -98,11 +98,6 @@ func (l *Logger) ComponentLogger(component string) *Logger { } } -// Deprecated: use ComponentLogger. -func (l *Logger) WithComponent(component string) *Logger { - return l.ComponentLogger(component) -} - // SetLevel changes the minimum log level. // // logger.SetLevel(LevelDebug) @@ -121,11 +116,6 @@ func (l *Logger) Level() Level { return l.level } -// Deprecated: use Level. -func (l *Logger) GetLevel() Level { - return l.Level() -} - // Fields represents key-value pairs for structured logging. // // fields := Fields{"peer_id": "node-1", "attempt": 2} @@ -261,11 +251,6 @@ func Global() *Logger { return globalLogger } -// Deprecated: use Global. -func GetGlobal() *Logger { - return Global() -} - // SetGlobalLevel changes the global logger level. // // SetGlobalLevel(LevelDebug) diff --git a/node/controller.go b/node/controller.go index 4fabe56..e273973 100644 --- a/node/controller.go +++ b/node/controller.go @@ -146,11 +146,6 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } -// Deprecated: use RemoteStats. -func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { - return c.RemoteStats(peerID) -} - // StartRemoteMiner requests a remote peer to start a miner with a given profile. // // err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) @@ -259,11 +254,6 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, return logs.Lines, nil } -// Deprecated: use RemoteLogs. -func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { - return c.RemoteLogs(peerID, minerName, lines) -} - // AllStats fetches stats from all connected peers. // // statsByPeerID := controller.AllStats() @@ -295,11 +285,6 @@ func (c *Controller) AllStats() map[string]*StatsPayload { return results } -// Deprecated: use AllStats. -func (c *Controller) GetAllStats() map[string]*StatsPayload { - return c.AllStats() -} - // PingPeer sends a ping to a peer and refreshes that peer's metrics. // // rttMS, err := controller.PingPeer("worker-1") diff --git a/node/controller_test.go b/node/controller_test.go index 075159e..e5906fd 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -136,7 +136,7 @@ func TestController_AutoConnect_Good(t *testing.T) { tp.ClientReg.AddPeer(peer) // Confirm no connection exists yet. - assert.Equal(t, 0, tp.Client.ConnectedPeers(), "should have no connections initially") + assert.Equal(t, 0, tp.Client.ConnectedPeerCount(), "should have no connections initially") // Send a request — controller should auto-connect via transport before sending. rtt, err := controller.PingPeer(serverIdentity.ID) @@ -144,10 +144,10 @@ func TestController_AutoConnect_Good(t *testing.T) { assert.Greater(t, rtt, 0.0, "RTT should be positive after auto-connect") // Verify connection was established. - assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection after auto-connect") + assert.Equal(t, 1, tp.Client.ConnectedPeerCount(), "should have 1 connection after auto-connect") } -func TestController_GetAllStats_Good(t *testing.T) { +func TestController_AllStats_Good(t *testing.T) { // Controller node with connections to two independent worker servers. controllerNM := newTestNodeManager(t, "controller", RoleController) controllerReg := newTestPeerRegistry(t) @@ -341,7 +341,7 @@ func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { "all concurrent requests to the same peer should succeed") } -func TestController_GetRemoteStats_Good(t *testing.T) { +func TestController_RemoteStats_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.Identity().ID @@ -368,7 +368,7 @@ func TestController_DisconnectFromPeer_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) serverID := tp.ServerNode.Identity().ID - assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection") + assert.Equal(t, 1, tp.Client.ConnectedPeerCount(), "should have 1 connection") err := controller.DisconnectFromPeer(serverID) require.NoError(t, err, "DisconnectFromPeer should succeed") @@ -590,7 +590,7 @@ func TestController_StopRemoteMiner_NoIdentity_Bad(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_GetRemoteLogs_Good(t *testing.T) { +func TestController_RemoteLogs_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.Identity().ID @@ -601,7 +601,7 @@ func TestController_GetRemoteLogs_Good(t *testing.T) { assert.Contains(t, lines[0], "started") } -func TestController_GetRemoteLogs_LimitedLines_Good(t *testing.T) { +func TestController_RemoteLogs_LimitedLines_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.Identity().ID @@ -610,7 +610,7 @@ func TestController_GetRemoteLogs_LimitedLines_Good(t *testing.T) { assert.Len(t, lines, 1, "should return only 1 line") } -func TestController_GetRemoteLogs_NoIdentity_Bad(t *testing.T) { +func TestController_RemoteLogs_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) @@ -623,7 +623,7 @@ func TestController_GetRemoteLogs_NoIdentity_Bad(t *testing.T) { assert.Contains(t, err.Error(), "identity not initialized") } -func TestController_GetRemoteStats_WithMiners_Good(t *testing.T) { +func TestController_RemoteStats_WithMiners_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.Identity().ID @@ -637,7 +637,7 @@ func TestController_GetRemoteStats_WithMiners_Good(t *testing.T) { assert.Equal(t, 1234.5, stats.Miners[0].Hashrate) } -func TestController_GetRemoteStats_NoIdentity_Bad(t *testing.T) { +func TestController_RemoteStats_NoIdentity_Bad(t *testing.T) { tp := setupTestTransportPair(t) keyPath, configPath := testNodeManagerPaths(t.TempDir()) nmNoID, err := NewNodeManagerFromPaths(keyPath, configPath) @@ -671,7 +671,7 @@ func TestController_ConnectToPeer_Success_Good(t *testing.T) { err := controller.ConnectToPeer(serverIdentity.ID) require.NoError(t, err, "ConnectToPeer should succeed") - assert.Equal(t, 1, tp.Client.ConnectedPeers(), "should have 1 connection after ConnectToPeer") + assert.Equal(t, 1, tp.Client.ConnectedPeerCount(), "should have 1 connection after ConnectToPeer") } func TestController_HandleResponse_NonReply_Good(t *testing.T) { diff --git a/node/dispatcher.go b/node/dispatcher.go index 769217e..85d1110 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -145,19 +145,10 @@ var ( // the safety threshold. ErrorThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) - // Deprecated: use ErrorThreatScoreExceeded. - ErrThreatScoreExceeded = ErrorThreatScoreExceeded - // ErrorUnknownIntent is returned when no handler is registered for the // packet's IntentID. ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - // Deprecated: use ErrorUnknownIntent. - ErrUnknownIntent = ErrorUnknownIntent - // ErrorNilPacket is returned when a nil packet is passed to Dispatch. ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) - - // Deprecated: use ErrorNilPacket. - ErrNilPacket = ErrorNilPacket ) diff --git a/node/dispatcher_test.go b/node/dispatcher_test.go index c65d1b1..1cbe6e3 100644 --- a/node/dispatcher_test.go +++ b/node/dispatcher_test.go @@ -78,13 +78,13 @@ func TestDispatcher_ThreatCircuitBreaker_Good(t *testing.T) { { name: "score just above threshold is rejected", threatScore: ThreatScoreThreshold + 1, - wantErr: ErrThreatScoreExceeded, + wantErr: ErrorThreatScoreExceeded, dispatched: false, }, { name: "maximum uint16 score is rejected", threatScore: 65535, - wantErr: ErrThreatScoreExceeded, + wantErr: ErrorThreatScoreExceeded, dispatched: false, }, { @@ -130,7 +130,7 @@ func TestDispatcher_UnknownIntentDropped_Bad(t *testing.T) { pkt := makePacket(0x42, 0, []byte("unknown")) err := d.Dispatch(pkt) - assert.ErrorIs(t, err, ErrUnknownIntent) + assert.ErrorIs(t, err, ErrorUnknownIntent) } func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { @@ -193,10 +193,10 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { } func TestDispatcher_NilAndEmptyPayload_Ugly(t *testing.T) { - t.Run("nil packet returns ErrNilPacket", func(t *testing.T) { + t.Run("nil packet returns ErrorNilPacket", func(t *testing.T) { d := NewDispatcher() err := d.Dispatch(nil) - assert.ErrorIs(t, err, ErrNilPacket) + assert.ErrorIs(t, err, ErrorNilPacket) }) t.Run("nil payload is delivered to handler", func(t *testing.T) { @@ -327,13 +327,13 @@ func TestDispatcher_ReplaceHandler_Good(t *testing.T) { func TestDispatcher_ThreatBlocksBeforeRouting_Good(t *testing.T) { // Verify that the circuit breaker fires before intent routing, - // so even an unknown intent returns ErrThreatScoreExceeded (not ErrUnknownIntent). + // so even an unknown intent returns ErrorThreatScoreExceeded (not ErrorUnknownIntent). d := NewDispatcher() pkt := makePacket(0x42, ThreatScoreThreshold+1, []byte("hostile")) err := d.Dispatch(pkt) - assert.ErrorIs(t, err, ErrThreatScoreExceeded, + assert.ErrorIs(t, err, ErrorThreatScoreExceeded, "threat circuit breaker should fire before intent routing") } diff --git a/node/errors.go b/node/errors.go index 1ee3f82..7d29af4 100644 --- a/node/errors.go +++ b/node/errors.go @@ -8,13 +8,7 @@ var ( // a node identity but none has been generated or loaded. ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // Deprecated: use ErrorIdentityNotInitialized. - ErrIdentityNotInitialized = ErrorIdentityNotInitialized - // ErrorMinerManagerNotConfigured is returned when a miner operation is // attempted but no MinerManager has been set on the Worker. ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) - - // Deprecated: use ErrorMinerManagerNotConfigured. - ErrMinerManagerNotConfigured = ErrorMinerManagerNotConfigured ) diff --git a/node/identity.go b/node/identity.go index 0198c3b..9a285c3 100644 --- a/node/identity.go +++ b/node/identity.go @@ -126,11 +126,6 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } -// Deprecated: use NewNodeManagerFromPaths. -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - // HasIdentity returns true if a node identity has been initialized. func (n *NodeManager) HasIdentity() bool { n.mu.RLock() @@ -152,11 +147,6 @@ func (n *NodeManager) Identity() *NodeIdentity { return &identity } -// Deprecated: use Identity. -func (n *NodeManager) GetIdentity() *NodeIdentity { - return n.Identity() -} - // GenerateIdentity writes a new node identity for the given name and role. // // err := nodeManager.GenerateIdentity("worker-1", RoleWorker) diff --git a/node/integration_test.go b/node/integration_test.go index 45116e5..96de5d2 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -116,9 +116,9 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // Allow server-side goroutines to register the connection. time.Sleep(100 * time.Millisecond) - assert.Equal(t, 1, controllerTransport.ConnectedPeers(), + assert.Equal(t, 1, controllerTransport.ConnectedPeerCount(), "controller should have 1 connected peer") - assert.Equal(t, 1, workerTransport.ConnectedPeers(), + assert.Equal(t, 1, workerTransport.ConnectedPeerCount(), "worker should have 1 connected peer") // Verify the peer's real identity is stored. @@ -199,7 +199,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { parsed3, err := ueps.ReadAndVerify(bufio.NewReader(bytes.NewReader(wireData3)), sharedSecret) require.NoError(t, err) err = dispatcher.Dispatch(parsed3) - assert.ErrorIs(t, err, ErrThreatScoreExceeded, + assert.ErrorIs(t, err, ErrorThreatScoreExceeded, "high-threat packet should be dropped by circuit breaker") // Compute handler should NOT have been called again. assert.Equal(t, int32(1), computeReceived.Load()) @@ -232,7 +232,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { time.Sleep(200 * time.Millisecond) // After graceful close, the controller should have 0 peers. - assert.Equal(t, 0, controllerTransport.ConnectedPeers(), + assert.Equal(t, 0, controllerTransport.ConnectedPeerCount(), "controller should have 0 peers after graceful close") } @@ -310,7 +310,7 @@ func TestIntegration_MultiPeerTopology_Good(t *testing.T) { } time.Sleep(100 * time.Millisecond) - assert.Equal(t, numWorkers, controllerTransport.ConnectedPeers(), + assert.Equal(t, numWorkers, controllerTransport.ConnectedPeerCount(), "controller should be connected to all workers") controller := NewController(controllerNM, controllerReg, controllerTransport) @@ -651,9 +651,9 @@ func TestIntegration_MessageSerialiseDeserialise_Good(t *testing.T) { assert.Equal(t, originalStats, decryptedStats) } -// TestIntegration_GetRemoteStats_EndToEnd tests the full stats retrieval flow +// TestIntegration_RemoteStats_EndToEnd tests the full stats retrieval flow // across a real WebSocket connection. -func TestIntegration_GetRemoteStats_EndToEnd_Good(t *testing.T) { +func TestIntegration_RemoteStats_EndToEnd_Good(t *testing.T) { tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) diff --git a/node/levin/connection_test.go b/node/levin/connection_test.go index 30af742..2b355f4 100644 --- a/node/levin/connection_test.go +++ b/node/levin/connection_test.go @@ -73,7 +73,7 @@ func TestConnection_Response_Good(t *testing.T) { receiver := NewConnection(b) payload := []byte("response data") - retCode := ReturnErrFormat + retCode := ReturnErrorFormat errCh := make(chan error, 1) go func() { @@ -120,7 +120,7 @@ func TestConnection_PayloadTooBig_Bad(t *testing.T) { _, _, err := receiver.ReadPacket() require.Error(t, err) - assert.ErrorIs(t, err, ErrPayloadTooBig) + assert.ErrorIs(t, err, ErrorPayloadTooBig) require.NoError(t, <-errCh) } diff --git a/node/levin/header.go b/node/levin/header.go index b8b465a..2f69577 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -26,15 +26,6 @@ const ( ReturnErrorConnection int32 = -1 ReturnErrorFormat int32 = -7 ReturnErrorSignature int32 = -13 - - // Deprecated: use ReturnErrorConnection. - ReturnErrConnection = ReturnErrorConnection - - // Deprecated: use ReturnErrorFormat. - ReturnErrFormat = ReturnErrorFormat - - // Deprecated: use ReturnErrorSignature. - ReturnErrSignature = ReturnErrorSignature ) // Command IDs for the CryptoNote P2P layer. @@ -54,12 +45,6 @@ const ( var ( ErrorBadSignature = core.E("levin", "bad signature", nil) ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) - - // Deprecated: use ErrorBadSignature. - ErrBadSignature = ErrorBadSignature - - // Deprecated: use ErrorPayloadTooBig. - ErrPayloadTooBig = ErrorPayloadTooBig ) // Header is the 33-byte packed header that prefixes every Levin message. diff --git a/node/levin/header_test.go b/node/levin/header_test.go index 9288977..39acdd0 100644 --- a/node/levin/header_test.go +++ b/node/levin/header_test.go @@ -74,11 +74,11 @@ func TestHeader_EncodeHeader_NegativeReturnCode_Good(t *testing.T) { PayloadSize: 0, ExpectResponse: false, Command: CommandHandshake, - ReturnCode: ReturnErrFormat, + ReturnCode: ReturnErrorFormat, } buf := EncodeHeader(h) rc := int32(binary.LittleEndian.Uint32(buf[21:25])) - assert.Equal(t, ReturnErrFormat, rc) + assert.Equal(t, ReturnErrorFormat, rc) } func TestHeader_DecodeHeader_RoundTrip_Ugly(t *testing.T) { @@ -87,7 +87,7 @@ func TestHeader_DecodeHeader_RoundTrip_Ugly(t *testing.T) { PayloadSize: 1024, ExpectResponse: true, Command: CommandTimedSync, - ReturnCode: ReturnErrConnection, + ReturnCode: ReturnErrorConnection, Flags: 0, ProtocolVersion: 0, } @@ -140,7 +140,7 @@ func TestHeader_DecodeHeader_BadSignature_Bad(t *testing.T) { buf := EncodeHeader(h) _, err := DecodeHeader(buf) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadSignature) + assert.ErrorIs(t, err, ErrorBadSignature) } func TestHeader_DecodeHeader_PayloadTooBig_Bad(t *testing.T) { @@ -152,7 +152,7 @@ func TestHeader_DecodeHeader_PayloadTooBig_Bad(t *testing.T) { buf := EncodeHeader(h) _, err := DecodeHeader(buf) require.Error(t, err) - assert.ErrorIs(t, err, ErrPayloadTooBig) + assert.ErrorIs(t, err, ErrorPayloadTooBig) } func TestHeader_DecodeHeader_MaxPayloadExact_Ugly(t *testing.T) { diff --git a/node/levin/storage.go b/node/levin/storage.go index 1e2cfb3..a6c74e2 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -46,24 +46,6 @@ var ( ErrorStorageNameTooLong = core.E("levin.storage", "entry name exceeds 255 bytes", nil) ErrorStorageTypeMismatch = core.E("levin.storage", "value type mismatch", nil) ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) - - // Deprecated: use ErrorStorageBadSignature. - ErrStorageBadSignature = ErrorStorageBadSignature - - // Deprecated: use ErrorStorageTruncated. - ErrStorageTruncated = ErrorStorageTruncated - - // Deprecated: use ErrorStorageBadVersion. - ErrStorageBadVersion = ErrorStorageBadVersion - - // Deprecated: use ErrorStorageNameTooLong. - ErrStorageNameTooLong = ErrorStorageNameTooLong - - // Deprecated: use ErrorStorageTypeMismatch. - ErrStorageTypeMismatch = ErrorStorageTypeMismatch - - // Deprecated: use ErrorStorageUnknownType. - ErrStorageUnknownType = ErrorStorageUnknownType ) // Section is an ordered map of named values forming a portable storage section. @@ -190,54 +172,6 @@ func ObjectArrayValue(vs []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: vs} } -// Deprecated: use Uint64Value. -func Uint64Val(v uint64) Value { return Uint64Value(v) } - -// Deprecated: use Uint32Value. -func Uint32Val(v uint32) Value { return Uint32Value(v) } - -// Deprecated: use Uint16Value. -func Uint16Val(v uint16) Value { return Uint16Value(v) } - -// Deprecated: use Uint8Value. -func Uint8Val(v uint8) Value { return Uint8Value(v) } - -// Deprecated: use Int64Value. -func Int64Val(v int64) Value { return Int64Value(v) } - -// Deprecated: use Int32Value. -func Int32Val(v int32) Value { return Int32Value(v) } - -// Deprecated: use Int16Value. -func Int16Val(v int16) Value { return Int16Value(v) } - -// Deprecated: use Int8Value. -func Int8Val(v int8) Value { return Int8Value(v) } - -// Deprecated: use BoolValue. -func BoolVal(v bool) Value { return BoolValue(v) } - -// Deprecated: use DoubleValue. -func DoubleVal(v float64) Value { return DoubleValue(v) } - -// Deprecated: use StringValue. -func StringVal(v []byte) Value { return StringValue(v) } - -// Deprecated: use ObjectValue. -func ObjectVal(s Section) Value { return ObjectValue(s) } - -// Deprecated: use Uint64ArrayValue. -func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } - -// Deprecated: use Uint32ArrayValue. -func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } - -// Deprecated: use StringArrayValue. -func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } - -// Deprecated: use ObjectArrayValue. -func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } - // --------------------------------------------------------------------------- // Scalar accessors // --------------------------------------------------------------------------- diff --git a/node/levin/varint.go b/node/levin/varint.go index 740a0df..5160646 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -25,15 +25,9 @@ const ( // ErrorVarintTruncated is returned when the buffer is too short. var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) -// Deprecated: use ErrorVarintTruncated. -var ErrVarintTruncated = ErrorVarintTruncated - // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// Deprecated: use ErrorVarintOverflow. -var ErrVarintOverflow = ErrorVarintOverflow - // PackVarint encodes v using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; // the remaining bits carry the value in little-endian order. diff --git a/node/levin/varint_test.go b/node/levin/varint_test.go index 948e4bd..d2088eb 100644 --- a/node/levin/varint_test.go +++ b/node/levin/varint_test.go @@ -82,7 +82,7 @@ func TestVarint_RoundTrip_Ugly(t *testing.T) { func TestVarint_UnpackVarint_EmptyInput_Ugly(t *testing.T) { _, _, err := UnpackVarint([]byte{}) require.Error(t, err) - assert.ErrorIs(t, err, ErrVarintTruncated) + assert.ErrorIs(t, err, ErrorVarintTruncated) } func TestVarint_UnpackVarint_Truncated2Byte_Bad(t *testing.T) { @@ -91,7 +91,7 @@ func TestVarint_UnpackVarint_Truncated2Byte_Bad(t *testing.T) { require.Len(t, buf, 2) _, _, err := UnpackVarint(buf[:1]) require.Error(t, err) - assert.ErrorIs(t, err, ErrVarintTruncated) + assert.ErrorIs(t, err, ErrorVarintTruncated) } func TestVarint_UnpackVarint_Truncated4Byte_Bad(t *testing.T) { @@ -99,7 +99,7 @@ func TestVarint_UnpackVarint_Truncated4Byte_Bad(t *testing.T) { require.Len(t, buf, 4) _, _, err := UnpackVarint(buf[:2]) require.Error(t, err) - assert.ErrorIs(t, err, ErrVarintTruncated) + assert.ErrorIs(t, err, ErrorVarintTruncated) } func TestVarint_UnpackVarint_Truncated8Byte_Bad(t *testing.T) { @@ -107,7 +107,7 @@ func TestVarint_UnpackVarint_Truncated8Byte_Bad(t *testing.T) { require.Len(t, buf, 8) _, _, err := UnpackVarint(buf[:4]) require.Error(t, err) - assert.ErrorIs(t, err, ErrVarintTruncated) + assert.ErrorIs(t, err, ErrorVarintTruncated) } func TestVarint_UnpackVarint_ExtraBytes_Good(t *testing.T) { diff --git a/node/message.go b/node/message.go index c321cc2..8b7f37c 100644 --- a/node/message.go +++ b/node/message.go @@ -90,53 +90,6 @@ const ( MessageError MessageType = "error" ) -const ( - // Deprecated: use MessageHandshake. - MsgHandshake = MessageHandshake - - // Deprecated: use MessageHandshakeAck. - MsgHandshakeAck = MessageHandshakeAck - - // Deprecated: use MessagePing. - MsgPing = MessagePing - - // Deprecated: use MessagePong. - MsgPong = MessagePong - - // Deprecated: use MessageDisconnect. - MsgDisconnect = MessageDisconnect - - // Deprecated: use MessageGetStats. - MsgGetStats = MessageGetStats - - // Deprecated: use MessageStats. - MsgStats = MessageStats - - // Deprecated: use MessageStartMiner. - MsgStartMiner = MessageStartMiner - - // Deprecated: use MessageStopMiner. - MsgStopMiner = MessageStopMiner - - // Deprecated: use MessageMinerAck. - MsgMinerAck = MessageMinerAck - - // Deprecated: use MessageDeploy. - MsgDeploy = MessageDeploy - - // Deprecated: use MessageDeployAck. - MsgDeployAck = MessageDeployAck - - // Deprecated: use MessageGetLogs. - MsgGetLogs = MessageGetLogs - - // Deprecated: use MessageLogs. - MsgLogs = MessageLogs - - // Deprecated: use MessageError. - MsgError = MessageError -) - // Message represents a P2P message between nodes. // // message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) @@ -295,9 +248,6 @@ type LogsRequestPayload struct { Since int64 `json:"since,omitempty"` // Unix timestamp, logs after this time } -// Deprecated: use LogsRequestPayload. -type GetLogsPayload = LogsRequestPayload - // LogsPayload contains console log lines. // // payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} @@ -343,24 +293,6 @@ const ( ErrorCodeNotFound = 1003 ErrorCodeOperationFailed = 1004 ErrorCodeTimeout = 1005 - - // Deprecated: use ErrorCodeUnknown. - ErrCodeUnknown = ErrorCodeUnknown - - // Deprecated: use ErrorCodeInvalidMessage. - ErrCodeInvalidMessage = ErrorCodeInvalidMessage - - // Deprecated: use ErrorCodeUnauthorized. - ErrCodeUnauthorized = ErrorCodeUnauthorized - - // Deprecated: use ErrorCodeNotFound. - ErrCodeNotFound = ErrorCodeNotFound - - // Deprecated: use ErrorCodeOperationFailed. - ErrCodeOperationFailed = ErrorCodeOperationFailed - - // Deprecated: use ErrorCodeTimeout. - ErrCodeTimeout = ErrorCodeTimeout ) // NewErrorMessage builds an error response message for an existing request. diff --git a/node/peer.go b/node/peer.go index 2071360..39d8867 100644 --- a/node/peer.go +++ b/node/peer.go @@ -163,11 +163,6 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } -// Deprecated: use NewPeerRegistryFromPath. -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - // SetAuthMode changes how unknown peers are handled. // // registry.SetAuthMode(PeerAuthAllowlist) @@ -187,11 +182,6 @@ func (r *PeerRegistry) AuthMode() PeerAuthMode { return r.authMode } -// Deprecated: use AuthMode. -func (r *PeerRegistry) GetAuthMode() PeerAuthMode { - return r.AuthMode() -} - // AllowPublicKey adds a public key to the allowlist. func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() @@ -350,11 +340,6 @@ func (r *PeerRegistry) Peer(id string) *Peer { return &peerCopy } -// Deprecated: use Peer. -func (r *PeerRegistry) GetPeer(id string) *Peer { - return r.Peer(id) -} - // ListPeers returns all registered peers. func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) @@ -434,11 +419,6 @@ func (r *PeerRegistry) MarkConnected(id string, connected bool) { } } -// Deprecated: use MarkConnected. -func (r *PeerRegistry) SetConnected(id string, connected bool) { - r.MarkConnected(id, connected) -} - // Score adjustment constants const ( ScoreSuccessIncrement = 1.0 // Increment for successful interaction @@ -529,11 +509,6 @@ func (r *PeerRegistry) PeersSortedByScore() []*Peer { return peers } -// Deprecated: use PeersSortedByScore. -func (r *PeerRegistry) GetPeersByScore() []*Peer { - return r.PeersSortedByScore() -} - // PeersByScore returns an iterator over peers sorted by score (highest first). func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { @@ -606,11 +581,6 @@ func (r *PeerRegistry) ConnectedPeerList() []*Peer { return slices.Collect(r.ConnectedPeers()) } -// Deprecated: use ConnectedPeerList. -func (r *PeerRegistry) GetConnectedPeers() []*Peer { - return r.ConnectedPeerList() -} - // ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { diff --git a/node/peer_test.go b/node/peer_test.go index 2365b32..e6799be 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -72,7 +72,7 @@ func TestPeer_Registry_AddPeer_Good(t *testing.T) { } } -func TestPeer_Registry_GetPeer_Good(t *testing.T) { +func TestPeer_Registry_Peer_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -242,7 +242,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { } } -func TestPeer_Registry_SetConnected_Good(t *testing.T) { +func TestPeer_Registry_MarkConnected_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -254,7 +254,7 @@ func TestPeer_Registry_SetConnected_Good(t *testing.T) { pr.AddPeer(peer) - pr.SetConnected("connect-test", true) + pr.MarkConnected("connect-test", true) updated := pr.Peer("connect-test") if updated == nil { @@ -267,7 +267,7 @@ func TestPeer_Registry_SetConnected_Good(t *testing.T) { t.Error("LastSeen should be set when connected") } - pr.SetConnected("connect-test", false) + pr.MarkConnected("connect-test", false) updated = pr.Peer("connect-test") if updated == nil { t.Fatal("expected peer to exist") @@ -277,7 +277,7 @@ func TestPeer_Registry_SetConnected_Good(t *testing.T) { } } -func TestPeer_Registry_GetConnectedPeers_Good(t *testing.T) { +func TestPeer_Registry_ConnectedPeerList_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -291,8 +291,8 @@ func TestPeer_Registry_GetConnectedPeers_Good(t *testing.T) { pr.AddPeer(p) } - pr.SetConnected("conn-1", true) - pr.SetConnected("conn-3", true) + pr.MarkConnected("conn-1", true) + pr.MarkConnected("conn-3", true) connected := pr.ConnectedPeerList() if len(connected) != 2 { @@ -604,7 +604,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { } } -func TestPeer_Registry_GetPeersByScore_Good(t *testing.T) { +func TestPeer_Registry_PeersSortedByScore_Good(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() @@ -800,12 +800,12 @@ func TestPeer_Registry_SelectNearestPeers_EmptyRegistry_Ugly(t *testing.T) { } } -func TestPeer_Registry_SetConnected_NonExistent_Bad(t *testing.T) { +func TestPeer_Registry_MarkConnected_NonExistent_Bad(t *testing.T) { pr, cleanup := setupTestPeerRegistry(t) defer cleanup() // Should not panic for non-existent peer - pr.SetConnected("ghost-peer", true) + pr.MarkConnected("ghost-peer", true) } func TestPeer_Registry_Close_NoDirtyData_Ugly(t *testing.T) { diff --git a/node/protocol.go b/node/protocol.go index bcd4a65..9c30b1e 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -97,8 +97,3 @@ func ProtocolErrorCode(err error) int { } return 0 } - -// Deprecated: use ProtocolErrorCode. -func GetProtocolErrorCode(err error) int { - return ProtocolErrorCode(err) -} diff --git a/node/transport.go b/node/transport.go index 99660ce..ce3c8e1 100644 --- a/node/transport.go +++ b/node/transport.go @@ -499,11 +499,6 @@ func (t *Transport) Connection(peerID string) *PeerConnection { return t.conns[peerID] } -// Deprecated: use Connection. -func (t *Transport) GetConnection(peerID string) *PeerConnection { - return t.Connection(peerID) -} - // handleWSUpgrade handles incoming WebSocket connections. func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -1051,8 +1046,3 @@ func (t *Transport) ConnectedPeerCount() int { defer t.mu.RUnlock() return len(t.conns) } - -// Deprecated: use ConnectedPeerCount. -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} diff --git a/node/transport_test.go b/node/transport_test.go index d47f7d9..fa90ab0 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -219,11 +219,11 @@ func TestTransport_FullHandshake_Good(t *testing.T) { // Allow server goroutines to register the connection time.Sleep(50 * time.Millisecond) - if tp.Server.ConnectedPeers() != 1 { - t.Errorf("server connected peers: got %d, want 1", tp.Server.ConnectedPeers()) + if tp.Server.ConnectedPeerCount() != 1 { + t.Errorf("server connected peers: got %d, want 1", tp.Server.ConnectedPeerCount()) } - if tp.Client.ConnectedPeers() != 1 { - t.Errorf("client connected peers: got %d, want 1", tp.Client.ConnectedPeers()) + if tp.Client.ConnectedPeerCount() != 1 { + t.Errorf("client connected peers: got %d, want 1", tp.Client.ConnectedPeerCount()) } // Verify peer identity was exchanged correctly @@ -539,8 +539,8 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { // Verify connection is established time.Sleep(50 * time.Millisecond) - if tp.Server.ConnectedPeers() != 1 { - t.Fatalf("server should have 1 peer initially, got %d", tp.Server.ConnectedPeers()) + if tp.Server.ConnectedPeerCount() != 1 { + t.Fatalf("server should have 1 peer initially, got %d", tp.Server.ConnectedPeerCount()) } // Close the underlying WebSocket on the client side to simulate network failure. @@ -558,9 +558,9 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { for { select { case <-deadline: - t.Fatalf("server did not clean up connection: still has %d peers", tp.Server.ConnectedPeers()) + t.Fatalf("server did not clean up connection: still has %d peers", tp.Server.ConnectedPeerCount()) default: - if tp.Server.ConnectedPeers() == 0 { + if tp.Server.ConnectedPeerCount() == 0 { // Verify registry updated peer := tp.ServerReg.Peer(clientID) if peer != nil && peer.Connected { @@ -749,18 +749,18 @@ func TestTransport_NewTransport_DefaultMaxMessageSize_Good(t *testing.T) { // The actual default is applied at usage time (readLoop, handleWSUpgrade) } -func TestTransport_ConnectedPeers_Good(t *testing.T) { +func TestTransport_ConnectedPeerCount_Good(t *testing.T) { tp := setupTestTransportPair(t) - if tp.Server.ConnectedPeers() != 0 { - t.Errorf("expected 0 connected peers initially, got %d", tp.Server.ConnectedPeers()) + if tp.Server.ConnectedPeerCount() != 0 { + t.Errorf("expected 0 connected peers initially, got %d", tp.Server.ConnectedPeerCount()) } tp.connectClient(t) time.Sleep(50 * time.Millisecond) - if tp.Server.ConnectedPeers() != 1 { - t.Errorf("expected 1 connected peer after connect, got %d", tp.Server.ConnectedPeers()) + if tp.Server.ConnectedPeerCount() != 1 { + t.Errorf("expected 1 connected peer after connect, got %d", tp.Server.ConnectedPeerCount()) } } diff --git a/node/worker.go b/node/worker.go index 884296f..916010a 100644 --- a/node/worker.go +++ b/node/worker.go @@ -419,8 +419,3 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } - -// Deprecated: use RegisterOnTransport. -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} -- 2.45.3 From f5d1ee6021152f548e5c92c14f1cfd34512c05fe Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:34:57 +0000 Subject: [PATCH 20/60] refactor(node): align worker interfaces with AX naming Co-Authored-By: Virgil --- node/bufpool_test.go | 2 +- node/controller_test.go | 14 ++++---- node/identity_test.go | 2 +- node/protocol_test.go | 2 +- node/worker.go | 42 +++++++++++----------- node/worker_test.go | 80 ++++++++++++++++++++--------------------- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/node/bufpool_test.go b/node/bufpool_test.go index 548761c..3f931d7 100644 --- a/node/bufpool_test.go +++ b/node/bufpool_test.go @@ -12,7 +12,7 @@ import ( // --- bufpool.go tests --- -func TestBufpool_GetBuffer_ReturnsResetBuffer_Good(t *testing.T) { +func TestBufpool_Buffer_ReturnsResetBuffer_Good(t *testing.T) { t.Run("buffer is initially empty", func(t *testing.T) { buf := getBuffer() defer putBuffer(buf) diff --git a/node/controller_test.go b/node/controller_test.go index e5906fd..b4a670c 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -444,7 +444,7 @@ func setupControllerPairWithMiner(t *testing.T) (*Controller, *Worker, *testTran return controller, worker, tp } -// mockMinerManagerFull implements MinerManager with functional start/stop/list/get. +// mockMinerManagerFull implements MinerManager with functional start/stop/list/lookup. type mockMinerManagerFull struct { mu sync.Mutex miners map[string]*mockMinerFull @@ -490,13 +490,13 @@ func (m *mockMinerManagerFull) ListMiners() []MinerInstance { return result } -func (m *mockMinerManagerFull) GetMiner(name string) (MinerInstance, error) { +func (m *mockMinerManagerFull) Miner(name string) (MinerInstance, error) { m.mu.Lock() defer m.mu.Unlock() miner, exists := m.miners[name] if !exists { - return nil, core.E("mockMinerManagerFull.GetMiner", "miner "+name+" not found", nil) + return nil, core.E("mockMinerManagerFull.Miner", "miner "+name+" not found", nil) } return miner, nil } @@ -509,10 +509,10 @@ type mockMinerFull struct { consoleHistory []string } -func (m *mockMinerFull) GetName() string { return m.name } -func (m *mockMinerFull) GetType() string { return m.minerType } -func (m *mockMinerFull) GetStats() (any, error) { return m.stats, nil } -func (m *mockMinerFull) GetConsoleHistory(lines int) []string { +func (m *mockMinerFull) Name() string { return m.name } +func (m *mockMinerFull) Type() string { return m.minerType } +func (m *mockMinerFull) Stats() (any, error) { return m.stats, nil } +func (m *mockMinerFull) ConsoleHistory(lines int) []string { if lines >= len(m.consoleHistory) { return m.consoleHistory } diff --git a/node/identity_test.go b/node/identity_test.go index 259d316..d4be4cb 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -338,7 +338,7 @@ func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { } } -func TestIdentity_NodeManager_GetIdentity_NilWhenNoIdentity_Bad(t *testing.T) { +func TestIdentity_NodeManager_Identity_NilWhenNoIdentity_Bad(t *testing.T) { nm := newTestNodeManagerWithoutIdentity(t) identity := nm.Identity() diff --git a/node/protocol_test.go b/node/protocol_test.go index 2fd3074..d609d9f 100644 --- a/node/protocol_test.go +++ b/node/protocol_test.go @@ -154,7 +154,7 @@ func TestProtocol_ConvenienceFunctions_Good(t *testing.T) { } } -func TestProtocol_GetProtocolErrorCode_NonProtocolError_Bad(t *testing.T) { +func TestProtocol_ProtocolErrorCode_NonProtocolError_Bad(t *testing.T) { err := core.NewError("regular error") if ProtocolErrorCode(err) != 0 { t.Error("Expected 0 for non-ProtocolError") diff --git a/node/worker.go b/node/worker.go index 916010a..ae26bcb 100644 --- a/node/worker.go +++ b/node/worker.go @@ -18,24 +18,24 @@ type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error ListMiners() []MinerInstance - GetMiner(name string) (MinerInstance, error) + Miner(name string) (MinerInstance, error) } // MinerInstance represents a running miner for stats collection. // // var miner MinerInstance type MinerInstance interface { - GetName() string - GetType() string - GetStats() (any, error) - GetConsoleHistory(lines int) []string + Name() string + Type() string + Stats() (any, error) + ConsoleHistory(lines int) []string } // ProfileManager interface for profile operations. // // var profileManager ProfileManager type ProfileManager interface { - GetProfile(id string) (any, error) + Profile(id string) (any, error) SaveProfile(profile any) error } @@ -88,13 +88,13 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { case MessagePing: response, err = w.handlePing(msg) case MessageGetStats: - response, err = w.handleGetStats(msg) + response, err = w.handleStats(msg) case MessageStartMiner: response, err = w.handleStartMiner(msg) case MessageStopMiner: response, err = w.handleStopMiner(msg) case MessageGetLogs: - response, err = w.handleGetLogs(msg) + response, err = w.handleLogs(msg) case MessageDeploy: response, err = w.handleDeploy(conn, msg) default: @@ -143,8 +143,8 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { return msg.Reply(MessagePong, pong) } -// handleGetStats responds with current miner statistics. -func (w *Worker) handleGetStats(msg *Message) (*Message, error) { +// handleStats responds with current miner statistics. +func (w *Worker) handleStats(msg *Message) (*Message, error) { identity := w.node.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized @@ -160,7 +160,7 @@ func (w *Worker) handleGetStats(msg *Message) (*Message, error) { if w.minerManager != nil { miners := w.minerManager.ListMiners() for _, miner := range miners { - minerStats, err := miner.GetStats() + minerStats, err := miner.Stats() if err != nil { continue } @@ -178,8 +178,8 @@ func (w *Worker) handleGetStats(msg *Message) (*Message, error) { // convertMinerStats converts miner stats to the protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ - Name: miner.GetName(), - Type: miner.GetType(), + Name: miner.Name(), + Type: miner.Type(), } // Try to extract common fields from the stats @@ -228,7 +228,7 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { if payload.Config != nil { config = payload.Config } else if w.profileManager != nil { - profile, err := w.profileManager.GetProfile(payload.ProfileID) + profile, err := w.profileManager.Profile(payload.ProfileID) if err != nil { return nil, core.E("Worker.handleStartMiner", "profile not found: "+payload.ProfileID, nil) } @@ -249,7 +249,7 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { ack := MinerAckPayload{ Success: true, - MinerName: miner.GetName(), + MinerName: miner.Name(), } return msg.Reply(MessageMinerAck, ack) } @@ -277,15 +277,15 @@ func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { return msg.Reply(MessageMinerAck, ack) } -// handleGetLogs returns console logs from a miner. -func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { +// handleLogs returns console logs from a miner. +func (w *Worker) handleLogs(msg *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured } var payload LogsRequestPayload if err := msg.ParsePayload(&payload); err != nil { - return nil, core.E("Worker.handleGetLogs", "invalid get logs payload", err) + return nil, core.E("Worker.handleLogs", "invalid logs payload", err) } // Validate and limit the Lines parameter to prevent resource exhaustion @@ -294,12 +294,12 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { payload.Lines = maxLogLines } - miner, err := w.minerManager.GetMiner(payload.MinerName) + miner, err := w.minerManager.Miner(payload.MinerName) if err != nil { - return nil, core.E("Worker.handleGetLogs", "miner not found: "+payload.MinerName, nil) + return nil, core.E("Worker.handleLogs", "miner not found: "+payload.MinerName, nil) } - lines := miner.GetConsoleHistory(payload.Lines) + lines := miner.ConsoleHistory(payload.Lines) logs := LogsPayload{ MinerName: payload.MinerName, diff --git a/node/worker_test.go b/node/worker_test.go index af2e921..122224c 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -169,7 +169,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { } } -func TestWorker_HandleGetStats_Good(t *testing.T) { +func TestWorker_HandleStats_Good(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -191,24 +191,24 @@ func TestWorker_HandleGetStats_Good(t *testing.T) { worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - // Create a get_stats message + // Create a stats request message. identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") } msg, err := NewMessage(MessageGetStats, "sender-id", identity.ID, nil) if err != nil { - t.Fatalf("failed to create get_stats message: %v", err) + t.Fatalf("failed to create stats request message: %v", err) } - // Call handleGetStats directly (without miner manager) - response, err := worker.handleGetStats(msg) + // Call handleStats directly (without miner manager). + response, err := worker.handleStats(msg) if err != nil { - t.Fatalf("handleGetStats returned error: %v", err) + t.Fatalf("handleStats returned error: %v", err) } if response == nil { - t.Fatal("handleGetStats returned nil response") + t.Fatal("handleStats returned nil response") } if response.Type != MessageStats { @@ -309,7 +309,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { } } -func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { +func TestWorker_HandleLogs_NoManager_Bad(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -331,7 +331,7 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { worker := NewWorker(nm, transport) worker.DataDir = t.TempDir() - // Create a get_logs message + // Create a logs request message. identity := nm.Identity() if identity == nil { t.Fatal("expected identity to be generated") @@ -339,11 +339,11 @@ func TestWorker_HandleGetLogs_NoManager_Bad(t *testing.T) { payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100} msg, err := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) if err != nil { - t.Fatalf("failed to create get_logs message: %v", err) + t.Fatalf("failed to create logs request message: %v", err) } // Without miner manager, should return error - _, err = worker.handleGetLogs(msg) + _, err = worker.handleLogs(msg) if err == nil { t.Error("expected error when miner manager is nil") } @@ -499,9 +499,9 @@ func (m *mockMinerManager) ListMiners() []MinerInstance { return m.miners } -func (m *mockMinerManager) GetMiner(name string) (MinerInstance, error) { +func (m *mockMinerManager) Miner(name string) (MinerInstance, error) { for _, miner := range m.miners { - if miner.GetName() == name { + if miner.Name() == name { return miner, nil } } @@ -514,14 +514,14 @@ type mockMinerInstance struct { stats any } -func (m *mockMinerInstance) GetName() string { return m.name } -func (m *mockMinerInstance) GetType() string { return m.minerType } -func (m *mockMinerInstance) GetStats() (any, error) { return m.stats, nil } -func (m *mockMinerInstance) GetConsoleHistory(lines int) []string { return []string{} } +func (m *mockMinerInstance) Name() string { return m.name } +func (m *mockMinerInstance) Type() string { return m.minerType } +func (m *mockMinerInstance) Stats() (any, error) { return m.stats, nil } +func (m *mockMinerInstance) ConsoleHistory(lines int) []string { return []string{} } type mockProfileManager struct{} -func (m *mockProfileManager) GetProfile(id string) (any, error) { +func (m *mockProfileManager) Profile(id string) (any, error) { return nil, nil } @@ -544,8 +544,8 @@ func (m *mockMinerManagerFailing) StopMiner(name string) error { return core.E("mockMinerManagerFailing.StopMiner", "miner "+name+" not found", nil) } -func (m *mockMinerManagerFailing) GetMiner(name string) (MinerInstance, error) { - return nil, core.E("mockMinerManagerFailing.GetMiner", "miner "+name+" not found", nil) +func (m *mockMinerManagerFailing) Miner(name string) (MinerInstance, error) { + return nil, core.E("mockMinerManagerFailing.Miner", "miner "+name+" not found", nil) } // mockProfileManagerFull implements ProfileManager that returns real data. @@ -553,10 +553,10 @@ type mockProfileManagerFull struct { profiles map[string]any } -func (m *mockProfileManagerFull) GetProfile(id string) (any, error) { +func (m *mockProfileManagerFull) Profile(id string) (any, error) { p, ok := m.profiles[id] if !ok { - return nil, core.E("mockProfileManagerFull.GetProfile", "profile "+id+" not found", nil) + return nil, core.E("mockProfileManagerFull.Profile", "profile "+id+" not found", nil) } return p, nil } @@ -568,8 +568,8 @@ func (m *mockProfileManagerFull) SaveProfile(profile any) error { // mockProfileManagerFailing always returns errors. type mockProfileManagerFailing struct{} -func (m *mockProfileManagerFailing) GetProfile(id string) (any, error) { - return nil, core.E("mockProfileManagerFailing.GetProfile", "profile "+id+" not found", nil) +func (m *mockProfileManagerFailing) Profile(id string) (any, error) { + return nil, core.E("mockProfileManagerFailing.Profile", "profile "+id+" not found", nil) } func (m *mockProfileManagerFailing) SaveProfile(profile any) error { @@ -809,7 +809,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { }) } -func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { +func TestWorker_HandleLogs_WithManager_Good(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -844,9 +844,9 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100} msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) - response, err := worker.handleGetLogs(msg) + response, err := worker.handleLogs(msg) if err != nil { - t.Fatalf("handleGetLogs returned error: %v", err) + t.Fatalf("handleLogs returned error: %v", err) } if response.Type != MessageLogs { @@ -861,14 +861,14 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { }) t.Run("MinerNotFound", func(t *testing.T) { - // Use a manager that returns error for GetMiner + // Use a manager that returns an error when the miner is missing. mm := &mockMinerManagerFailing{} worker.SetMinerManager(mm) payload := LogsRequestPayload{MinerName: "non-existent", Lines: 50} msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) - _, err := worker.handleGetLogs(msg) + _, err := worker.handleLogs(msg) if err == nil { t.Error("expected error for non-existent miner") } @@ -885,9 +885,9 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { payload := LogsRequestPayload{MinerName: "test-miner", Lines: -1} msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) - response, err := worker.handleGetLogs(msg) + response, err := worker.handleLogs(msg) if err != nil { - t.Fatalf("handleGetLogs returned error: %v", err) + t.Fatalf("handleLogs returned error: %v", err) } // Lines <= 0 should be clamped to maxLogLines if response.Type != MessageLogs { @@ -906,9 +906,9 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { payload := LogsRequestPayload{MinerName: "test-miner", Lines: 999999} msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload) - response, err := worker.handleGetLogs(msg) + response, err := worker.handleLogs(msg) if err != nil { - t.Fatalf("handleGetLogs returned error: %v", err) + t.Fatalf("handleLogs returned error: %v", err) } if response.Type != MessageLogs { t.Errorf("expected %s, got %s", MessageLogs, response.Type) @@ -916,7 +916,7 @@ func TestWorker_HandleGetLogs_WithManager_Good(t *testing.T) { }) } -func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { +func TestWorker_HandleStats_WithMinerManager_Good(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -964,9 +964,9 @@ func TestWorker_HandleGetStats_WithMinerManager_Good(t *testing.T) { worker.SetMinerManager(mm) msg, _ := NewMessage(MessageGetStats, "sender-id", identity.ID, nil) - response, err := worker.handleGetStats(msg) + response, err := worker.handleStats(msg) if err != nil { - t.Fatalf("handleGetStats returned error: %v", err) + t.Fatalf("handleStats returned error: %v", err) } var stats StatsPayload @@ -1308,7 +1308,7 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { } } -func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { +func TestWorker_HandleStats_NoIdentity_Bad(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -1324,7 +1324,7 @@ func TestWorker_HandleGetStats_NoIdentity_Bad(t *testing.T) { worker.DataDir = t.TempDir() msg, _ := NewMessage(MessageGetStats, "sender-id", "target-id", nil) - _, err := worker.handleGetStats(msg) + _, err := worker.handleStats(msg) if err == nil { t.Error("expected error when identity is not initialized") } @@ -1353,8 +1353,8 @@ func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) { } } -func TestWorker_HandleMessage_GetStats_IntegrationViaWebSocket_Good(t *testing.T) { - // HandleMessage dispatch for get_stats through real WebSocket +func TestWorker_HandleMessage_Stats_IntegrationViaWebSocket_Good(t *testing.T) { + // HandleMessage dispatch for stats through real WebSocket. tp := setupTestTransportPair(t) worker := NewWorker(tp.ServerNode, tp.Server) -- 2.45.3 From de8b138367677264df8366655dd2670b474269d7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:39:15 +0000 Subject: [PATCH 21/60] refactor(ueps): clarify layer tag names Co-Authored-By: Virgil --- ueps/packet.go | 18 +++++++++--------- ueps/packet_coverage_test.go | 4 ++-- ueps/packet_test.go | 8 ++++---- ueps/reader.go | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ueps/packet.go b/ueps/packet.go index f03578b..be9d440 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -12,13 +12,13 @@ import ( // TLV Types const ( - TagVersion = 0x01 - TagCurrentLay = 0x02 - TagTargetLay = 0x03 - TagIntent = 0x04 - TagThreatScore = 0x05 - TagHMAC = 0x06 // The Signature - TagPayload = 0xFF // The Data + TagVersion = 0x01 + TagCurrentLayer = 0x02 + TagTargetLayer = 0x03 + TagIntent = 0x04 + TagThreatScore = 0x05 + TagHMAC = 0x06 // The Signature + TagPayload = 0xFF // The Data ) // UEPSHeader represents the conscious routing metadata @@ -65,10 +65,10 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } - if err := writeTLV(buf, TagCurrentLay, []byte{p.Header.CurrentLayer}); err != nil { + if err := writeTLV(buf, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagTargetLay, []byte{p.Header.TargetLayer}); err != nil { + if err := writeTLV(buf, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { return nil, err } if err := writeTLV(buf, TagIntent, []byte{p.Header.IntentID}); err != nil { diff --git a/ueps/packet_coverage_test.go b/ueps/packet_coverage_test.go index 0748420..6e3eced 100644 --- a/ueps/packet_coverage_test.go +++ b/ueps/packet_coverage_test.go @@ -182,8 +182,8 @@ func TestPacketCoverage_ReadAndVerify_ManualPacket_PayloadReadError_Bad(t *testi // Build header TLVs var hdr bytes.Buffer require.NoError(t, writeTLV(&hdr, TagVersion, []byte{0x09})) - require.NoError(t, writeTLV(&hdr, TagCurrentLay, []byte{5})) - require.NoError(t, writeTLV(&hdr, TagTargetLay, []byte{5})) + require.NoError(t, writeTLV(&hdr, TagCurrentLayer, []byte{5})) + require.NoError(t, writeTLV(&hdr, TagTargetLayer, []byte{5})) require.NoError(t, writeTLV(&hdr, TagIntent, []byte{0x20})) tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, 0) diff --git a/ueps/packet_test.go b/ueps/packet_test.go index 89c7729..449819e 100644 --- a/ueps/packet_test.go +++ b/ueps/packet_test.go @@ -201,8 +201,8 @@ func TestPacket_MissingHMACTag_Bad(t *testing.T) { // Write header TLVs writeTLV(&buf, TagVersion, []byte{0x09}) - writeTLV(&buf, TagCurrentLay, []byte{5}) - writeTLV(&buf, TagTargetLay, []byte{5}) + writeTLV(&buf, TagCurrentLayer, []byte{5}) + writeTLV(&buf, TagTargetLayer, []byte{5}) writeTLV(&buf, TagIntent, []byte{0x20}) tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, 0) @@ -285,8 +285,8 @@ func TestPacket_UnknownTLVTag_Bad(t *testing.T) { // Standard header TLVs writeTLV(&headerBuf, TagVersion, []byte{0x09}) - writeTLV(&headerBuf, TagCurrentLay, []byte{5}) - writeTLV(&headerBuf, TagTargetLay, []byte{5}) + writeTLV(&headerBuf, TagCurrentLayer, []byte{5}) + writeTLV(&headerBuf, TagTargetLayer, []byte{5}) writeTLV(&headerBuf, TagIntent, []byte{0x20}) tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, 0) diff --git a/ueps/reader.go b/ueps/reader.go index 6619003..e620ed1 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -58,12 +58,12 @@ func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) - case TagCurrentLay: + case TagCurrentLayer: header.CurrentLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) - case TagTargetLay: + case TagTargetLayer: header.TargetLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) -- 2.45.3 From dec79b54d6fedc99c093bb0839d2e87fae4fd8b1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:50:35 +0000 Subject: [PATCH 22/60] refactor(node): clarify filesystem and buffer pool names Co-Authored-By: Virgil --- node/{bufpool.go => buffer_pool.go} | 4 +-- node/{bufpool_test.go => buffer_pool_test.go} | 26 +++++++++---------- node/{core_fs.go => filesystem.go} | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) rename node/{bufpool.go => buffer_pool.go} (90%) rename node/{bufpool_test.go => buffer_pool_test.go} (85%) rename node/{core_fs.go => filesystem.go} (94%) diff --git a/node/bufpool.go b/node/buffer_pool.go similarity index 90% rename from node/bufpool.go rename to node/buffer_pool.go index 49d98ac..ace5915 100644 --- a/node/bufpool.go +++ b/node/buffer_pool.go @@ -7,8 +7,8 @@ import ( core "dappco.re/go/core" ) -// bufferPool provides reusable byte buffers for JSON encoding. -// This reduces allocation overhead in hot paths like message serialization. +// bufferPool provides reusable byte buffers for JSON encoding in hot paths. +// This reduces allocation overhead in message serialization. var bufferPool = sync.Pool{ New: func() any { return bytes.NewBuffer(make([]byte, 0, 1024)) diff --git a/node/bufpool_test.go b/node/buffer_pool_test.go similarity index 85% rename from node/bufpool_test.go rename to node/buffer_pool_test.go index 3f931d7..35e1cc5 100644 --- a/node/bufpool_test.go +++ b/node/buffer_pool_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" ) -// --- bufpool.go tests --- +// --- buffer_pool.go tests --- -func TestBufpool_Buffer_ReturnsResetBuffer_Good(t *testing.T) { +func TestBufferPool_Buffer_ReturnsResetBuffer_Good(t *testing.T) { t.Run("buffer is initially empty", func(t *testing.T) { buf := getBuffer() defer putBuffer(buf) @@ -33,7 +33,7 @@ func TestBufpool_Buffer_ReturnsResetBuffer_Good(t *testing.T) { }) } -func TestBufpool_PutBuffer_DiscardsOversizedBuffers_Good(t *testing.T) { +func TestBufferPool_PutBuffer_DiscardsOversizedBuffers_Good(t *testing.T) { t.Run("buffer at 64KB limit is pooled", func(t *testing.T) { buf := getBuffer() buf.Grow(65536) @@ -59,7 +59,7 @@ func TestBufpool_PutBuffer_DiscardsOversizedBuffers_Good(t *testing.T) { }) } -func TestBufpool_BufPool_BufferIndependence_Good(t *testing.T) { +func TestBufferPool_BufferIndependence_Good(t *testing.T) { buf1 := getBuffer() buf2 := getBuffer() @@ -77,7 +77,7 @@ func TestBufpool_BufPool_BufferIndependence_Good(t *testing.T) { putBuffer(buf2) } -func TestBufpool_MarshalJSON_BasicTypes_Good(t *testing.T) { +func TestBufferPool_MarshalJSON_BasicTypes_Good(t *testing.T) { tests := []struct { name string input any @@ -129,7 +129,7 @@ func TestBufpool_MarshalJSON_BasicTypes_Good(t *testing.T) { } } -func TestBufpool_MarshalJSON_NoTrailingNewline_Good(t *testing.T) { +func TestBufferPool_MarshalJSON_NoTrailingNewline_Good(t *testing.T) { data, err := MarshalJSON(map[string]string{"key": "value"}) require.NoError(t, err) @@ -137,7 +137,7 @@ func TestBufpool_MarshalJSON_NoTrailingNewline_Good(t *testing.T) { "MarshalJSON should strip the trailing newline added by json.Encoder") } -func TestBufpool_MarshalJSON_HTMLEscaping_Good(t *testing.T) { +func TestBufferPool_MarshalJSON_HTMLEscaping_Good(t *testing.T) { input := map[string]string{"html": ""} data, err := MarshalJSON(input) require.NoError(t, err) @@ -146,7 +146,7 @@ func TestBufpool_MarshalJSON_HTMLEscaping_Good(t *testing.T) { "HTML characters should not be escaped when EscapeHTML is false") } -func TestBufpool_MarshalJSON_ReturnsCopy_Good(t *testing.T) { +func TestBufferPool_MarshalJSON_ReturnsCopy_Good(t *testing.T) { data1, err := MarshalJSON("first") require.NoError(t, err) @@ -161,7 +161,7 @@ func TestBufpool_MarshalJSON_ReturnsCopy_Good(t *testing.T) { "returned slice should be a copy and not be mutated by subsequent calls") } -func TestBufpool_MarshalJSON_ReturnsIndependentCopy_Good(t *testing.T) { +func TestBufferPool_MarshalJSON_ReturnsIndependentCopy_Good(t *testing.T) { data1, err := MarshalJSON(map[string]string{"first": "call"}) require.NoError(t, err) @@ -174,13 +174,13 @@ func TestBufpool_MarshalJSON_ReturnsIndependentCopy_Good(t *testing.T) { "second result should contain its own data") } -func TestBufpool_MarshalJSON_InvalidValue_Bad(t *testing.T) { +func TestBufferPool_MarshalJSON_InvalidValue_Bad(t *testing.T) { ch := make(chan int) _, err := MarshalJSON(ch) assert.Error(t, err, "marshalling an unserialisable type should return an error") } -func TestBufpool_BufferPool_ConcurrentAccess_Ugly(t *testing.T) { +func TestBufferPool_ConcurrentAccess_Ugly(t *testing.T) { const goroutines = 100 const iterations = 50 @@ -205,7 +205,7 @@ func TestBufpool_BufferPool_ConcurrentAccess_Ugly(t *testing.T) { wg.Wait() } -func TestBufpool_MarshalJSON_ConcurrentSafety_Ugly(t *testing.T) { +func TestBufferPool_MarshalJSON_ConcurrentSafety_Ugly(t *testing.T) { const goroutines = 50 var wg sync.WaitGroup @@ -241,7 +241,7 @@ func TestBufpool_MarshalJSON_ConcurrentSafety_Ugly(t *testing.T) { } } -func TestBufpool_BufferPool_ReuseAfterReset_Ugly(t *testing.T) { +func TestBufferPool_ReuseAfterReset_Ugly(t *testing.T) { buf := getBuffer() buf.Write(make([]byte, 4096)) putBuffer(buf) diff --git a/node/core_fs.go b/node/filesystem.go similarity index 94% rename from node/core_fs.go rename to node/filesystem.go index 2deb2dd..4139b7b 100644 --- a/node/core_fs.go +++ b/node/filesystem.go @@ -51,5 +51,5 @@ func filesystemResultError(result core.Result) error { return err } - return core.E("node.fs", "filesystem operation failed", nil) + return core.E("node.filesystem", "filesystem operation failed", nil) } -- 2.45.3 From 819862a1a491f34a8ee3ff4deeebc15568e2f79e Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:31:11 +0000 Subject: [PATCH 23/60] refactor(node): tighten AX naming across core paths Co-Authored-By: Virgil --- node/bundle.go | 70 ++++----- node/levin/connection.go | 36 ++--- node/peer.go | 102 ++++++------- node/transport.go | 301 ++++++++++++++++++++------------------- 4 files changed, 250 insertions(+), 259 deletions(-) diff --git a/node/bundle.go b/node/bundle.go index edd725d..03a78ed 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -55,14 +55,14 @@ type BundleManifest struct { // bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config - t, err := tim.New() + timBundle, err := tim.New() if err != nil { return nil, core.E("CreateProfileBundle", "failed to create TIM", err) } - t.Config = profileJSON + timBundle.Config = profileJSON // Encrypt to STIM format - stimData, err := t.ToSigil(password) + stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateProfileBundle", "failed to encrypt bundle", err) } @@ -112,24 +112,24 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo } // Create DataNode from tarball - dn, err := datanode.FromTar(tarData) + dataNode, err := datanode.FromTar(tarData) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create datanode", err) } // Create TIM from DataNode - t, err := tim.FromDataNode(dn) + timBundle, err := tim.FromDataNode(dataNode) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create TIM", err) } // Set profile as config if provided if profileJSON != nil { - t.Config = profileJSON + timBundle.Config = profileJSON } // Encrypt to STIM format - stimData, err := t.ToSigil(password) + stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateMinerBundle", "failed to encrypt bundle", err) } @@ -159,12 +159,12 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { } // Decrypt STIM format - t, err := tim.FromSigil(bundle.Data, password) + timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return nil, core.E("ExtractProfileBundle", "failed to decrypt bundle", err) } - return t.Config, nil + return timBundle.Config, nil } // ExtractMinerBundle decrypts and extracts a miner bundle, returning the miner path and profile. @@ -177,13 +177,13 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string } // Decrypt STIM format - t, err := tim.FromSigil(bundle.Data, password) + timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to decrypt bundle", err) } // Convert rootfs to tarball and extract - tarData, err := t.RootFS.ToTar() + tarData, err := timBundle.RootFS.ToTar() if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) } @@ -194,7 +194,7 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string return "", nil, core.E("ExtractMinerBundle", "failed to extract tarball", err) } - return minerPath, t.Config, nil + return minerPath, timBundle.Config, nil } // VerifyBundle checks if a bundle's checksum is valid. @@ -222,24 +222,24 @@ func isJSON(data []byte) bool { // createTarball creates a tar archive from a map of filename -> content. func createTarball(files map[string][]byte) ([]byte, error) { var buf bytes.Buffer - tw := tar.NewWriter(&buf) + tarWriter := tar.NewWriter(&buf) // Track directories we've created - dirs := make(map[string]bool) + createdDirectories := make(map[string]bool) for name, content := range files { // Create parent directories if needed dir := core.PathDir(name) - if dir != "." && !dirs[dir] { - hdr := &tar.Header{ + if dir != "." && !createdDirectories[dir] { + header := &tar.Header{ Name: dir + "/", Mode: 0755, Typeflag: tar.TypeDir, } - if err := tw.WriteHeader(hdr); err != nil { + if err := tarWriter.WriteHeader(header); err != nil { return nil, err } - dirs[dir] = true + createdDirectories[dir] = true } // Determine file mode (executable for binaries in miners/) @@ -248,20 +248,20 @@ func createTarball(files map[string][]byte) ([]byte, error) { mode = 0755 } - hdr := &tar.Header{ + header := &tar.Header{ Name: name, Mode: mode, Size: int64(len(content)), } - if err := tw.WriteHeader(hdr); err != nil { + if err := tarWriter.WriteHeader(header); err != nil { return nil, err } - if _, err := tw.Write(content); err != nil { + if _, err := tarWriter.Write(content); err != nil { return nil, err } } - if err := tw.Close(); err != nil { + if err := tarWriter.Close(); err != nil { return nil, err } @@ -290,11 +290,11 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return "", err } - tr := tar.NewReader(bytes.NewReader(tarData)) + tarReader := tar.NewReader(bytes.NewReader(tarData)) var firstExecutable string for { - hdr, err := tr.Next() + header, err := tarReader.Next() if err == io.EOF { break } @@ -303,16 +303,16 @@ func extractTarball(tarData []byte, destDir string) (string, error) { } // Security: Sanitize the tar entry name to prevent path traversal (Zip Slip) - cleanName := core.CleanPath(hdr.Name, "/") + cleanName := core.CleanPath(header.Name, "/") // Reject absolute paths if core.PathIsAbs(cleanName) { - return "", core.E("extractTarball", "invalid tar entry: absolute path not allowed: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: absolute path not allowed: "+header.Name, nil) } // Reject paths that escape the destination directory if core.HasPrefix(cleanName, "../") || cleanName == ".." { - return "", core.E("extractTarball", "invalid tar entry: path traversal attempt: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: path traversal attempt: "+header.Name, nil) } // Build the full path and verify it's within destDir @@ -324,10 +324,10 @@ func extractTarball(tarData []byte, destDir string) (string, error) { allowedPrefix = absDestDir } if !core.HasPrefix(fullPath, allowedPrefix) && fullPath != absDestDir { - return "", core.E("extractTarball", "invalid tar entry: path escape attempt: "+hdr.Name, nil) + return "", core.E("extractTarball", "invalid tar entry: path escape attempt: "+header.Name, nil) } - switch hdr.Typeflag { + switch header.Typeflag { case tar.TypeDir: if err := filesystemEnsureDir(fullPath); err != nil { return "", err @@ -340,21 +340,21 @@ func extractTarball(tarData []byte, destDir string) (string, error) { // Limit file size to prevent decompression bombs (100MB max per file) const maxFileSize int64 = 100 * 1024 * 1024 - limitedReader := io.LimitReader(tr, maxFileSize+1) + limitedReader := io.LimitReader(tarReader, maxFileSize+1) content, err := io.ReadAll(limitedReader) if err != nil { - return "", core.E("extractTarball", "failed to write file "+hdr.Name, err) + return "", core.E("extractTarball", "failed to write file "+header.Name, err) } if int64(len(content)) > maxFileSize { filesystemDelete(fullPath) - return "", core.E("extractTarball", "file "+hdr.Name+" exceeds maximum size", nil) + return "", core.E("extractTarball", "file "+header.Name+" exceeds maximum size", nil) } - if err := filesystemResultError(localFileSystem.WriteMode(fullPath, string(content), fs.FileMode(hdr.Mode))); err != nil { - return "", core.E("extractTarball", "failed to create file "+hdr.Name, err) + if err := filesystemResultError(localFileSystem.WriteMode(fullPath, string(content), fs.FileMode(header.Mode))); err != nil { + return "", core.E("extractTarball", "failed to create file "+header.Name, err) } // Track first executable - if hdr.Mode&0111 != 0 && firstExecutable == "" { + if header.Mode&0111 != 0 && firstExecutable == "" { firstExecutable = fullPath } // Explicitly ignore symlinks and hard links to prevent symlink attacks diff --git a/node/levin/connection.go b/node/levin/connection.go index 9745f83..46670f2 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -41,8 +41,8 @@ type Connection struct { // WriteTimeout is the deadline applied before each write call. WriteTimeout time.Duration - conn net.Conn - writeMu sync.Mutex + conn net.Conn + writeMutex sync.Mutex } // NewConnection creates a Connection that wraps conn with sensible defaults. @@ -61,7 +61,7 @@ func NewConnection(conn net.Conn) *Connection { // // err := conn.WritePacket(CommandPing, payload, true) func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { - h := Header{ + header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: expectResponse, @@ -70,14 +70,14 @@ func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool Flags: FlagRequest, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&h, payload) + return c.writeFrame(&header, payload) } // WriteResponse sends a Levin response packet with the given return code. // // err := conn.WriteResponse(CommandPing, payload, ReturnOK) func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { - h := Header{ + header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: false, @@ -86,15 +86,15 @@ func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) Flags: FlagResponse, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&h, payload) + return c.writeFrame(&header, payload) } // writeFrame serialises header + payload and writes them atomically. -func (c *Connection) writeFrame(h *Header, payload []byte) error { - buf := EncodeHeader(h) +func (c *Connection) writeFrame(header *Header, payload []byte) error { + buf := EncodeHeader(header) - c.writeMu.Lock() - defer c.writeMu.Unlock() + c.writeMutex.Lock() + defer c.writeMutex.Unlock() if err := c.conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { return err @@ -122,32 +122,32 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } // Read header. - var hdrBuf [HeaderSize]byte - if _, err := io.ReadFull(c.conn, hdrBuf[:]); err != nil { + var headerBytes [HeaderSize]byte + if _, err := io.ReadFull(c.conn, headerBytes[:]); err != nil { return Header{}, nil, err } - h, err := DecodeHeader(hdrBuf) + header, err := DecodeHeader(headerBytes) if err != nil { return Header{}, nil, err } // Check against the connection-specific payload limit. - if h.PayloadSize > c.MaxPayloadSize { + if header.PayloadSize > c.MaxPayloadSize { return Header{}, nil, ErrorPayloadTooBig } // Empty payload is valid — return nil data without allocation. - if h.PayloadSize == 0 { - return h, nil, nil + if header.PayloadSize == 0 { + return header, nil, nil } - payload := make([]byte, h.PayloadSize) + payload := make([]byte, header.PayloadSize) if _, err := io.ReadFull(c.conn, payload); err != nil { return Header{}, nil, err } - return h, payload, nil + return header, payload, nil } // Close closes the underlying network connection. diff --git a/node/peer.go b/node/peer.go index 39d8867..c43ea23 100644 --- a/node/peer.go +++ b/node/peer.go @@ -37,8 +37,8 @@ type Peer struct { Connected bool `json:"-"` } -// saveDebounceInterval is the minimum time between disk writes. -const saveDebounceInterval = 5 * time.Second +// peerRegistrySaveDebounceInterval is the minimum time between disk writes. +const peerRegistrySaveDebounceInterval = 5 * time.Second // PeerAuthMode controls how unknown peers are handled // @@ -58,8 +58,8 @@ const ( PeerNameMaxLength = 64 ) -// peerNameRegex validates peer names: alphanumeric, hyphens, underscores, and spaces -var peerNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-_ ]{0,62}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`) +// peerNamePattern validates peer names: alphanumeric, hyphens, underscores, and spaces. +var peerNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-_ ]{0,62}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`) // safeKeyPrefix returns a truncated key for logging, handling short keys safely func safeKeyPrefix(key string) string { @@ -85,7 +85,7 @@ func validatePeerName(name string) error { if len(name) > PeerNameMaxLength { return core.E("validatePeerName", "peer name too long", nil) } - if !peerNameRegex.MatchString(name) { + if !peerNamePattern.MatchString(name) { return core.E("validatePeerName", "peer name contains invalid characters (use alphanumeric, hyphens, underscores, spaces)", nil) } return nil @@ -106,11 +106,9 @@ type PeerRegistry struct { allowedPublicKeyMu sync.RWMutex // Protects allowedPublicKeys // Debounce disk writes - dirty bool // Whether there are unsaved changes - saveTimer *time.Timer // Timer for debounced save - saveMu sync.Mutex // Protects dirty and saveTimer - stopChan chan struct{} // Signal to stop background save - saveStopOnce sync.Once // Ensure stopChan is closed only once + hasPendingChanges bool // Whether there are unsaved changes + pendingSaveTimer *time.Timer // Timer for debounced save + saveMutex sync.Mutex // Protects pending save state } // Dimension weights for peer selection @@ -144,8 +142,7 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), path: peersPath, - stopChan: make(chan struct{}), - authMode: PeerAuthOpen, // Default to open for backward compatibility + authMode: PeerAuthOpen, // Default to open. allowedPublicKeys: make(map[string]bool), } @@ -286,7 +283,8 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { r.rebuildKDTree() r.mu.Unlock() - return r.save() + r.scheduleSave() + return nil } // UpdatePeer updates an existing peer's information. @@ -303,7 +301,8 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { r.rebuildKDTree() r.mu.Unlock() - return r.save() + r.scheduleSave() + return nil } // RemovePeer removes a peer from the registry. @@ -320,7 +319,8 @@ func (r *PeerRegistry) RemovePeer(id string) error { r.rebuildKDTree() r.mu.Unlock() - return r.save() + r.scheduleSave() + return nil } // Peer returns a copy of the peer with the supplied ID. @@ -363,7 +363,7 @@ func (r *PeerRegistry) Peers() iter.Seq[*Peer] { // UpdateMetrics updates a peer's performance metrics. // Note: Persistence is debounced. Call Close() to flush before shutdown. -func (r *PeerRegistry) UpdateMetrics(id string, pingMS, geoKM float64, hops int) error { +func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geoKilometres float64, hopCount int) error { r.mu.Lock() peer, exists := r.peers[id] @@ -372,15 +372,16 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMS, geoKM float64, hops int) return core.E("PeerRegistry.UpdateMetrics", "peer "+id+" not found", nil) } - peer.PingMS = pingMS - peer.GeoKM = geoKM - peer.Hops = hops + peer.PingMS = pingMilliseconds + peer.GeoKM = geoKilometres + peer.Hops = hopCount peer.LastSeen = time.Now() r.rebuildKDTree() r.mu.Unlock() - return r.save() + r.scheduleSave() + return nil } // UpdateScore updates a peer's reliability score. @@ -401,7 +402,8 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { r.rebuildKDTree() r.mu.Unlock() - return r.save() + r.scheduleSave() + return nil } // MarkConnected updates a peer's connection state. @@ -441,7 +443,7 @@ func (r *PeerRegistry) RecordSuccess(id string) { peer.Score = min(peer.Score+ScoreSuccessIncrement, ScoreMaximum) peer.LastSeen = time.Now() r.mu.Unlock() - r.save() + r.scheduleSave() } // RecordFailure records a failed interaction with a peer, reducing their score. @@ -456,7 +458,7 @@ func (r *PeerRegistry) RecordFailure(id string) { peer.Score = max(peer.Score-ScoreFailureDecrement, ScoreMinimum) newScore := peer.Score r.mu.Unlock() - r.save() + r.scheduleSave() logging.Debug("peer score decreased", logging.Fields{ "peer_id": id, @@ -477,7 +479,7 @@ func (r *PeerRegistry) RecordTimeout(id string) { peer.Score = max(peer.Score-ScoreTimeoutDecrement, ScoreMinimum) newScore := peer.Score r.mu.Unlock() - r.save() + r.scheduleSave() logging.Debug("peer score decreased", logging.Fields{ "peer_id": id, @@ -642,26 +644,26 @@ func (r *PeerRegistry) rebuildKDTree() { } // scheduleSave schedules a debounced save operation. -// Multiple calls within saveDebounceInterval will be coalesced into a single save. -// Must NOT be called with r.mu held. +// Multiple calls within peerRegistrySaveDebounceInterval will be coalesced into a single save. +// Call it after releasing r.mu so peer state and save state do not interleave. func (r *PeerRegistry) scheduleSave() { - r.saveMu.Lock() - defer r.saveMu.Unlock() + r.saveMutex.Lock() + defer r.saveMutex.Unlock() - r.dirty = true + r.hasPendingChanges = true // If timer already running, let it handle the save - if r.saveTimer != nil { + if r.pendingSaveTimer != nil { return } // Start a new timer - r.saveTimer = time.AfterFunc(saveDebounceInterval, func() { - r.saveMu.Lock() - r.saveTimer = nil - shouldSave := r.dirty - r.dirty = false - r.saveMu.Unlock() + r.pendingSaveTimer = time.AfterFunc(peerRegistrySaveDebounceInterval, func() { + r.saveMutex.Lock() + r.pendingSaveTimer = nil + shouldSave := r.hasPendingChanges + r.hasPendingChanges = false + r.saveMutex.Unlock() if shouldSave { r.mu.RLock() @@ -709,19 +711,15 @@ func (r *PeerRegistry) saveNow() error { // Close flushes any pending changes and releases resources. func (r *PeerRegistry) Close() error { - r.saveStopOnce.Do(func() { - close(r.stopChan) - }) - - // Cancel pending timer and save immediately if dirty - r.saveMu.Lock() - if r.saveTimer != nil { - r.saveTimer.Stop() - r.saveTimer = nil + // Cancel any pending timer and save immediately if changes are queued. + r.saveMutex.Lock() + if r.pendingSaveTimer != nil { + r.pendingSaveTimer.Stop() + r.pendingSaveTimer = nil } - shouldSave := r.dirty - r.dirty = false - r.saveMu.Unlock() + shouldSave := r.hasPendingChanges + r.hasPendingChanges = false + r.saveMutex.Unlock() if shouldSave { r.mu.RLock() @@ -733,14 +731,6 @@ func (r *PeerRegistry) Close() error { return nil } -// save is a helper that schedules a debounced save. -// Kept for backward compatibility but now debounces writes. -// Must NOT be called with r.mu held. -func (r *PeerRegistry) save() error { - r.scheduleSave() - return nil // Errors will be logged asynchronously -} - // load reads peers from disk. func (r *PeerRegistry) load() error { content, err := filesystemRead(r.path) diff --git a/node/transport.go b/node/transport.go index ce3c8e1..30ef318 100644 --- a/node/transport.go +++ b/node/transport.go @@ -21,11 +21,11 @@ import ( "github.com/gorilla/websocket" ) -// debugLogCounter tracks message counts for rate limiting debug logs -var debugLogCounter atomic.Int64 +// messageLogSampleCounter tracks message counts for sampled debug logs. +var messageLogSampleCounter atomic.Int64 -// debugLogInterval controls how often we log debug messages in hot paths (1 in N) -const debugLogInterval = 100 +// messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). +const messageLogSampleInterval = 100 // DefaultMaxMessageSize is the default maximum message size (1MB) const DefaultMaxMessageSize int64 = 1 << 20 // 1MB @@ -66,49 +66,49 @@ func DefaultTransportConfig() TransportConfig { // var handler MessageHandler = func(conn *PeerConnection, msg *Message) {} type MessageHandler func(conn *PeerConnection, msg *Message) -// MessageDeduplicator tracks seen message IDs to prevent duplicate processing +// MessageDeduplicator tracks recent message IDs to prevent duplicate processing. // // deduplicator := NewMessageDeduplicator(5 * time.Minute) type MessageDeduplicator struct { - seen map[string]time.Time - mu sync.RWMutex - ttl time.Duration + recentMessageTimes map[string]time.Time + mutex sync.RWMutex + timeToLive time.Duration } -// NewMessageDeduplicator creates a deduplicator with specified TTL +// NewMessageDeduplicator creates a deduplicator with the supplied retention window. // // deduplicator := NewMessageDeduplicator(5 * time.Minute) -func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator { +func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator { d := &MessageDeduplicator{ - seen: make(map[string]time.Time), - ttl: ttl, + recentMessageTimes: make(map[string]time.Time), + timeToLive: retentionWindow, } return d } -// IsDuplicate checks if a message ID has been seen recently +// IsDuplicate checks whether a message ID is still within the retention window. func (d *MessageDeduplicator) IsDuplicate(msgID string) bool { - d.mu.RLock() - _, exists := d.seen[msgID] - d.mu.RUnlock() + d.mutex.RLock() + _, exists := d.recentMessageTimes[msgID] + d.mutex.RUnlock() return exists } -// Mark records a message ID as seen +// Mark records a message ID as recently seen. func (d *MessageDeduplicator) Mark(msgID string) { - d.mu.Lock() - d.seen[msgID] = time.Now() - d.mu.Unlock() + d.mutex.Lock() + d.recentMessageTimes[msgID] = time.Now() + d.mutex.Unlock() } -// Cleanup removes expired entries +// Cleanup removes expired entries from the deduplicator. func (d *MessageDeduplicator) Cleanup() { - d.mu.Lock() - defer d.mu.Unlock() + d.mutex.Lock() + defer d.mutex.Unlock() now := time.Now() - for id, seen := range d.seen { - if now.Sub(seen) > d.ttl { - delete(d.seen, id) + for id, seenAt := range d.recentMessageTimes { + if now.Sub(seenAt) > d.timeToLive { + delete(d.recentMessageTimes, id) } } } @@ -117,61 +117,62 @@ func (d *MessageDeduplicator) Cleanup() { // // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) type Transport struct { - config TransportConfig - server *http.Server - upgrader websocket.Upgrader - conns map[string]*PeerConnection // peer ID -> connection - pendingConns atomic.Int32 // tracks connections during handshake - node *NodeManager - registry *PeerRegistry - handler MessageHandler - dedup *MessageDeduplicator // Message deduplication - mu sync.RWMutex - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + config TransportConfig + httpServer *http.Server + upgrader websocket.Upgrader + connections map[string]*PeerConnection // peer ID -> connection + pendingHandshakeCount atomic.Int32 // tracks connections during handshake + nodeManager *NodeManager + peerRegistry *PeerRegistry + messageHandler MessageHandler + messageDeduplicator *MessageDeduplicator // Message deduplication + mutex sync.RWMutex + lifecycleContext context.Context + cancelLifecycle context.CancelFunc + waitGroup sync.WaitGroup } -// PeerRateLimiter implements a simple token bucket rate limiter per peer +// PeerRateLimiter implements a simple token bucket rate limiter per peer. // // rateLimiter := NewPeerRateLimiter(100, 50) type PeerRateLimiter struct { - tokens int - maxTokens int - refillRate int // tokens per second - lastRefill time.Time - mu sync.Mutex + availableTokens int + capacity int + refillPerSecond int // tokens per second + lastRefillTime time.Time + mutex sync.Mutex } -// NewPeerRateLimiter creates a rate limiter with specified messages/second +// NewPeerRateLimiter creates a token bucket seeded with maxTokens and refilled +// at refillRate tokens per second. // // rateLimiter := NewPeerRateLimiter(100, 50) -func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter { +func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { return &PeerRateLimiter{ - tokens: maxTokens, - maxTokens: maxTokens, - refillRate: refillRate, - lastRefill: time.Now(), + availableTokens: maxTokens, + capacity: maxTokens, + refillPerSecond: refillPerSecond, + lastRefillTime: time.Now(), } } // Allow checks if a message is allowed and consumes a token if so func (r *PeerRateLimiter) Allow() bool { - r.mu.Lock() - defer r.mu.Unlock() + r.mutex.Lock() + defer r.mutex.Unlock() // Refill tokens based on elapsed time now := time.Now() - elapsed := now.Sub(r.lastRefill) - tokensToAdd := int(elapsed.Seconds()) * r.refillRate + elapsed := now.Sub(r.lastRefillTime) + tokensToAdd := int(elapsed.Seconds()) * r.refillPerSecond if tokensToAdd > 0 { - r.tokens = min(r.tokens+tokensToAdd, r.maxTokens) - r.lastRefill = now + r.availableTokens = min(r.availableTokens+tokensToAdd, r.capacity) + r.lastRefillTime = now } // Check if we have tokens available - if r.tokens > 0 { - r.tokens-- + if r.availableTokens > 0 { + r.availableTokens-- return true } return false @@ -186,7 +187,7 @@ type PeerConnection struct { SharedSecret []byte // Derived via X25519 ECDH, used for SMSG LastActivity time.Time UserAgent string // Request identity advertised by the peer - writeMu sync.Mutex // Serialize WebSocket writes + writeMutex sync.Mutex // Serialize WebSocket writes transport *Transport closeOnce sync.Once // Ensure Close() is only called once rateLimiter *PeerRateLimiter // Per-peer message rate limiting @@ -196,14 +197,14 @@ type PeerConnection struct { // // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { - ctx, cancel := context.WithCancel(context.Background()) + lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) return &Transport{ - config: config, - node: node, - registry: registry, - conns: make(map[string]*PeerConnection), - dedup: NewMessageDeduplicator(5 * time.Minute), // 5 minute TTL for dedup + config: config, + nodeManager: node, + peerRegistry: registry, + connections: make(map[string]*PeerConnection), + messageDeduplicator: NewMessageDeduplicator(5 * time.Minute), // 5 minute TTL for dedup upgrader: websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -222,8 +223,8 @@ func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportCon return host == "localhost" || host == "127.0.0.1" || host == "::1" }, }, - ctx: ctx, - cancel: cancel, + lifecycleContext: lifecycleContext, + cancelLifecycle: cancelLifecycle, } } @@ -263,7 +264,7 @@ func agentHeaderToken(value string) string { // agentUserAgent returns a transparent identity string for request headers. func (t *Transport) agentUserAgent() string { - identity := t.node.Identity() + identity := t.nodeManager.Identity() if identity == nil { return core.Sprintf("%s proto=%s", agentUserAgentPrefix, ProtocolVersion) } @@ -283,7 +284,7 @@ func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.WSPath, t.handleWSUpgrade) - t.server = &http.Server{ + t.httpServer = &http.Server{ Addr: t.config.ListenAddr, Handler: mux, ReadTimeout: 30 * time.Second, @@ -294,7 +295,7 @@ func (t *Transport) Start() error { // Apply TLS hardening if TLS is enabled if t.config.TLSCertPath != "" && t.config.TLSKeyPath != "" { - t.server.TLSConfig = &tls.Config{ + t.httpServer.TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ // TLS 1.3 ciphers (automatically used when available) @@ -316,12 +317,12 @@ func (t *Transport) Start() error { } } - t.wg.Go(func() { + t.waitGroup.Go(func() { var err error if t.config.TLSCertPath != "" && t.config.TLSKeyPath != "" { - err = t.server.ListenAndServeTLS(t.config.TLSCertPath, t.config.TLSKeyPath) + err = t.httpServer.ListenAndServeTLS(t.config.TLSCertPath, t.config.TLSKeyPath) } else { - err = t.server.ListenAndServe() + err = t.httpServer.ListenAndServe() } if err != nil && err != http.ErrServerClosed { logging.Error("HTTP server error", logging.Fields{"error": err, "addr": t.config.ListenAddr}) @@ -329,15 +330,15 @@ func (t *Transport) Start() error { }) // Start message deduplication cleanup goroutine - t.wg.Go(func() { + t.waitGroup.Go(func() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { - case <-t.ctx.Done(): + case <-t.lifecycleContext.Done(): return case <-ticker.C: - t.dedup.Cleanup() + t.messageDeduplicator.Cleanup() } } }) @@ -347,28 +348,28 @@ func (t *Transport) Start() error { // Stop closes active connections and shuts the transport down cleanly. func (t *Transport) Stop() error { - t.cancel() + t.cancelLifecycle() // Gracefully close all connections with shutdown message - t.mu.RLock() - conns := slices.Collect(maps.Values(t.conns)) - t.mu.RUnlock() + t.mutex.RLock() + connections := slices.Collect(maps.Values(t.connections)) + t.mutex.RUnlock() - for _, pc := range conns { + for _, pc := range connections { pc.GracefulClose("server shutdown", DisconnectShutdown) } // Shutdown HTTP server if it was started - if t.server != nil { + if t.httpServer != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := t.server.Shutdown(ctx); err != nil { + if err := t.httpServer.Shutdown(ctx); err != nil { return core.E("Transport.Stop", "server shutdown error", err) } } - t.wg.Wait() + t.waitGroup.Wait() return nil } @@ -376,9 +377,9 @@ func (t *Transport) Stop() error { // // transport.OnMessage(worker.HandleMessage) func (t *Transport) OnMessage(handler MessageHandler) { - t.mu.Lock() - defer t.mu.Unlock() - t.handler = handler + t.mutex.Lock() + defer t.mutex.Unlock() + t.messageHandler = handler } // Connect dials a peer, completes the handshake, and starts the session loops. @@ -419,9 +420,9 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } // Store connection using the real peer ID from handshake - t.mu.Lock() - t.conns[pc.Peer.ID] = pc - t.mu.Unlock() + t.mutex.Lock() + t.connections[pc.Peer.ID] = pc + t.mutex.Unlock() logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) logging.Debug("connected peer metadata", logging.Fields{ @@ -430,16 +431,16 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { }) // Update registry - t.registry.MarkConnected(pc.Peer.ID, true) + t.peerRegistry.MarkConnected(pc.Peer.ID, true) // Start read loop - t.wg.Add(1) + t.waitGroup.Add(1) go t.readLoop(pc) logging.Debug("started readLoop for peer", logging.Fields{"peer_id": pc.Peer.ID}) // Start keepalive - t.wg.Add(1) + t.waitGroup.Add(1) go t.keepalive(pc) return pc, nil @@ -447,9 +448,9 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Send transmits an encrypted message to a connected peer. func (t *Transport) Send(peerID string, msg *Message) error { - t.mu.RLock() - pc, exists := t.conns[peerID] - t.mu.RUnlock() + t.mutex.RLock() + pc, exists := t.connections[peerID] + t.mutex.RUnlock() if !exists { return core.E("Transport.Send", "peer "+peerID+" not connected", nil) @@ -461,10 +462,10 @@ func (t *Transport) Send(peerID string, msg *Message) error { // Connections returns an iterator over all active peer connections. func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { - t.mu.RLock() - defer t.mu.RUnlock() + t.mutex.RLock() + defer t.mutex.RUnlock() - for _, pc := range t.conns { + for _, pc := range t.connections { if !yield(pc) { return } @@ -494,9 +495,9 @@ func (t *Transport) Broadcast(msg *Message) error { // // connection := transport.Connection("worker-1") func (t *Transport) Connection(peerID string) *PeerConnection { - t.mu.RLock() - defer t.mu.RUnlock() - return t.conns[peerID] + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.connections[peerID] } // handleWSUpgrade handles incoming WebSocket connections. @@ -504,20 +505,20 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") // Enforce MaxConns limit (including pending connections during handshake) - t.mu.RLock() - currentConns := len(t.conns) - t.mu.RUnlock() - pendingConns := int(t.pendingConns.Load()) + t.mutex.RLock() + currentConnections := len(t.connections) + t.mutex.RUnlock() + pendingHandshakeCount := int(t.pendingHandshakeCount.Load()) - totalConns := currentConns + pendingConns - if totalConns >= t.config.MaxConns { + totalConnections := currentConnections + pendingHandshakeCount + if totalConnections >= t.config.MaxConns { http.Error(w, "Too many connections", http.StatusServiceUnavailable) return } // Track this connection as pending during handshake - t.pendingConns.Add(1) - defer t.pendingConns.Add(-1) + t.pendingHandshakeCount.Add(1) + defer t.pendingHandshakeCount.Add(-1) conn, err := t.upgrader.Upgrade(w, r, nil) if err != nil { @@ -568,7 +569,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "peer_id": payload.Identity.ID, "user_agent": userAgent, }) - identity := t.node.Identity() + identity := t.nodeManager.Identity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -585,14 +586,14 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Derive shared secret from peer's public key - sharedSecret, err := t.node.DeriveSharedSecret(payload.Identity.PublicKey) + sharedSecret, err := t.nodeManager.DeriveSharedSecret(payload.Identity.PublicKey) if err != nil { conn.Close() return } // Check if peer is allowed to connect (allowlist check) - if !t.registry.IsPeerAllowed(payload.Identity.ID, payload.Identity.PublicKey) { + if !t.peerRegistry.IsPeerAllowed(payload.Identity.ID, payload.Identity.PublicKey) { logging.Warn("peer connection rejected: not in allowlist", logging.Fields{ "peer_id": payload.Identity.ID, "peer_name": payload.Identity.Name, @@ -600,7 +601,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "user_agent": userAgent, }) // Send rejection before closing - identity := t.node.Identity() + identity := t.nodeManager.Identity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -617,7 +618,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Create peer if not exists (only if auth passed) - peer := t.registry.Peer(payload.Identity.ID) + peer := t.peerRegistry.Peer(payload.Identity.ID) if peer == nil { // Auto-register the peer since they passed allowlist check peer = &Peer{ @@ -628,7 +629,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { AddedAt: time.Now(), Score: 50, } - t.registry.AddPeer(peer) + t.peerRegistry.AddPeer(peer) logging.Info("auto-registered new peer", logging.Fields{ "peer_id": peer.ID, "peer_name": peer.Name, @@ -646,7 +647,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Send handshake acknowledgment - identity := t.node.Identity() + identity := t.nodeManager.Identity() if identity == nil { conn.Close() return @@ -683,12 +684,12 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Store connection - t.mu.Lock() - t.conns[peer.ID] = pc - t.mu.Unlock() + t.mutex.Lock() + t.connections[peer.ID] = pc + t.mutex.Unlock() // Update registry - t.registry.MarkConnected(peer.ID, true) + t.peerRegistry.MarkConnected(peer.ID, true) logging.Debug("accepted peer connection", logging.Fields{ "peer_id": peer.ID, @@ -697,11 +698,11 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { }) // Start read loop - t.wg.Add(1) + t.waitGroup.Add(1) go t.readLoop(pc) // Start keepalive - t.wg.Add(1) + t.waitGroup.Add(1) go t.keepalive(pc) } @@ -717,7 +718,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { pc.Conn.SetReadDeadline(time.Time{}) }() - identity := t.node.Identity() + identity := t.nodeManager.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -780,7 +781,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { pc.Peer.Role = ackPayload.Identity.Role // Verify challenge response - derive shared secret first using the peer's public key - sharedSecret, err := t.node.DeriveSharedSecret(pc.Peer.PublicKey) + sharedSecret, err := t.nodeManager.DeriveSharedSecret(pc.Peer.PublicKey) if err != nil { return core.E("Transport.performHandshake", "derive shared secret for challenge verification", err) } @@ -797,9 +798,9 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { pc.SharedSecret = sharedSecret // Update the peer in registry with the real identity - if err := t.registry.UpdatePeer(pc.Peer); err != nil { + if err := t.peerRegistry.UpdatePeer(pc.Peer); err != nil { // If update fails (peer not found with old ID), add as new - t.registry.AddPeer(pc.Peer) + t.peerRegistry.AddPeer(pc.Peer) } logging.Debug("handshake completed with challenge-response verification", logging.Fields{ @@ -813,7 +814,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { // readLoop reads messages from a peer connection. func (t *Transport) readLoop(pc *PeerConnection) { - defer t.wg.Done() + defer t.waitGroup.Done() defer t.removeConnection(pc) // Apply message size limit to prevent memory exhaustion attacks @@ -825,7 +826,7 @@ func (t *Transport) readLoop(pc *PeerConnection) { for { select { - case <-t.ctx.Done(): + case <-t.lifecycleContext.Done(): return default: } @@ -859,21 +860,21 @@ func (t *Transport) readLoop(pc *PeerConnection) { } // Check for duplicate messages (prevents amplification attacks) - if t.dedup.IsDuplicate(msg.ID) { + if t.messageDeduplicator.IsDuplicate(msg.ID) { logging.Debug("dropping duplicate message", logging.Fields{"msg_id": msg.ID, "peer_id": pc.Peer.ID}) continue } - t.dedup.Mark(msg.ID) + t.messageDeduplicator.Mark(msg.ID) // Rate limit debug logs in hot path to reduce noise (log 1 in N messages) - if debugLogCounter.Add(1)%debugLogInterval == 0 { + if messageLogSampleCounter.Add(1)%messageLogSampleInterval == 0 { logging.Debug("received message from peer", logging.Fields{"type": msg.Type, "peer_id": pc.Peer.ID, "reply_to": msg.ReplyTo, "sample": "1/100"}) } // Dispatch to handler (read handler under lock to avoid race) - t.mu.RLock() - handler := t.handler - t.mu.RUnlock() + t.mutex.RLock() + handler := t.messageHandler + t.mutex.RUnlock() if handler != nil { handler(pc, msg) } @@ -882,14 +883,14 @@ func (t *Transport) readLoop(pc *PeerConnection) { // keepalive sends periodic pings. func (t *Transport) keepalive(pc *PeerConnection) { - defer t.wg.Done() + defer t.waitGroup.Done() ticker := time.NewTicker(t.config.PingInterval) defer ticker.Stop() for { select { - case <-t.ctx.Done(): + case <-t.lifecycleContext.Done(): return case <-ticker.C: // Check if connection is still alive @@ -899,7 +900,7 @@ func (t *Transport) keepalive(pc *PeerConnection) { } // Send ping - identity := t.node.Identity() + identity := t.nodeManager.Identity() pingMsg, err := NewMessage(MessagePing, identity.ID, pc.Peer.ID, PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -917,18 +918,18 @@ func (t *Transport) keepalive(pc *PeerConnection) { // removeConnection removes and cleans up a connection. func (t *Transport) removeConnection(pc *PeerConnection) { - t.mu.Lock() - delete(t.conns, pc.Peer.ID) - t.mu.Unlock() + t.mutex.Lock() + delete(t.connections, pc.Peer.ID) + t.mutex.Unlock() - t.registry.MarkConnected(pc.Peer.ID, false) + t.peerRegistry.MarkConnected(pc.Peer.ID, false) pc.Close() } // Send sends an encrypted message over the connection. func (pc *PeerConnection) Send(msg *Message) error { - pc.writeMu.Lock() - defer pc.writeMu.Unlock() + pc.writeMutex.Lock() + defer pc.writeMutex.Unlock() // Encrypt message using SMSG data, err := pc.transport.encryptMessage(msg, pc.SharedSecret) @@ -976,11 +977,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { // Try to send disconnect message (best effort). - // Note: we must NOT call SetWriteDeadline outside writeMu — Send() + // Note: we must NOT call SetWriteDeadline outside writeMutex — Send() // already manages write deadlines under the lock. Setting it here // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { - identity := pc.transport.node.Identity() + identity := pc.transport.nodeManager.Identity() if identity != nil { payload := DisconnectPayload{ Reason: reason, @@ -1042,7 +1043,7 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, // // count := transport.ConnectedPeerCount() func (t *Transport) ConnectedPeerCount() int { - t.mu.RLock() - defer t.mu.RUnlock() - return len(t.conns) + t.mutex.RLock() + defer t.mutex.RUnlock() + return len(t.connections) } -- 2.45.3 From 851b1294bdafcdbca4fe8cbfa0732a43fc8427db Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:38:29 +0000 Subject: [PATCH 24/60] refactor(node): rename controller and worker core fields Co-Authored-By: Virgil --- node/controller.go | 94 ++++++++++++++++++++--------------------- node/controller_test.go | 24 +++++------ node/worker.go | 26 ++++++------ node/worker_test.go | 48 ++++++++++----------- 4 files changed, 96 insertions(+), 96 deletions(-) diff --git a/node/controller.go b/node/controller.go index e273973..07a62ae 100644 --- a/node/controller.go +++ b/node/controller.go @@ -14,24 +14,24 @@ import ( // // controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { - node *NodeManager - peers *PeerRegistry - transport *Transport - mu sync.RWMutex + nodeManager *NodeManager + peerRegistry *PeerRegistry + transport *Transport + mutex sync.RWMutex - // Pending requests awaiting responses - pending map[string]chan *Message // message ID -> response channel + // Pending requests awaiting responses. + pendingRequests map[string]chan *Message // message ID -> response channel } // NewController wires a controller to a node manager, peer registry, and transport. // // controller := NewController(nodeManager, peerRegistry, transport) -func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller { +func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transport *Transport) *Controller { c := &Controller{ - node: node, - peers: peers, - transport: transport, - pending: make(map[string]chan *Message), + nodeManager: nodeManager, + peerRegistry: peerRegistry, + transport: transport, + pendingRequests: make(map[string]chan *Message), } // Register message handler for responses @@ -46,16 +46,16 @@ func (c *Controller) handleResponse(_ *PeerConnection, msg *Message) { return // Not a response, let worker handle it } - c.mu.Lock() - responseCh, isPending := c.pending[msg.ReplyTo] - if isPending { - delete(c.pending, msg.ReplyTo) + c.mutex.Lock() + responseChannel, hasPendingRequest := c.pendingRequests[msg.ReplyTo] + if hasPendingRequest { + delete(c.pendingRequests, msg.ReplyTo) } - c.mu.Unlock() + c.mutex.Unlock() - if isPending && responseCh != nil { + if hasPendingRequest && responseChannel != nil { select { - case responseCh <- msg: + case responseChannel <- msg: default: // Late duplicate response; drop it. } @@ -73,7 +73,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat // Auto-connect if not already connected if c.transport.Connection(peerID) == nil { - peer := c.peers.Peer(peerID) + peer := c.peerRegistry.Peer(peerID) if peer == nil { return nil, core.E("Controller.sendRequest", "peer not found: "+peerID, nil) } @@ -88,18 +88,18 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat } // Create response channel - responseCh := make(chan *Message, 1) + responseChannel := make(chan *Message, 1) - c.mu.Lock() - c.pending[msg.ID] = responseCh - c.mu.Unlock() + c.mutex.Lock() + c.pendingRequests[msg.ID] = responseChannel + c.mutex.Unlock() // Clean up on exit. Deleting the pending entry is enough because // handleResponse only routes through the map. defer func() { - c.mu.Lock() - delete(c.pending, msg.ID) - c.mu.Unlock() + c.mutex.Lock() + delete(c.pendingRequests, msg.ID) + c.mutex.Unlock() }() // Send the message @@ -112,8 +112,8 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat defer cancel() select { - case resp := <-responseCh: - return resp, nil + case response := <-responseChannel: + return response, nil case <-ctx.Done(): return nil, core.E("Controller.sendRequest", "request timeout", nil) } @@ -123,7 +123,7 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat // // stats, err := controller.RemoteStats("worker-1") func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { - identity := c.node.Identity() + identity := c.nodeManager.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -133,13 +133,13 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { return nil, core.E("Controller.RemoteStats", "failed to create message", err) } - resp, err := c.sendRequest(peerID, msg, 10*time.Second) + response, err := c.sendRequest(peerID, msg, 10*time.Second) if err != nil { return nil, err } var stats StatsPayload - if err := ParseResponse(resp, MessageStats, &stats); err != nil { + if err := ParseResponse(response, MessageStats, &stats); err != nil { return nil, err } @@ -150,7 +150,7 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { // // err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { - identity := c.node.Identity() + identity := c.nodeManager.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -170,13 +170,13 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi return core.E("Controller.StartRemoteMiner", "failed to create message", err) } - resp, err := c.sendRequest(peerID, msg, 30*time.Second) + response, err := c.sendRequest(peerID, msg, 30*time.Second) if err != nil { return err } var ack MinerAckPayload - if err := ParseResponse(resp, MessageMinerAck, &ack); err != nil { + if err := ParseResponse(response, MessageMinerAck, &ack); err != nil { return err } @@ -191,7 +191,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi // // err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { - identity := c.node.Identity() + identity := c.nodeManager.Identity() if identity == nil { return ErrorIdentityNotInitialized } @@ -205,13 +205,13 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return core.E("Controller.StopRemoteMiner", "failed to create message", err) } - resp, err := c.sendRequest(peerID, msg, 30*time.Second) + response, err := c.sendRequest(peerID, msg, 30*time.Second) if err != nil { return err } var ack MinerAckPayload - if err := ParseResponse(resp, MessageMinerAck, &ack); err != nil { + if err := ParseResponse(response, MessageMinerAck, &ack); err != nil { return err } @@ -226,7 +226,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { // // logs, err := controller.RemoteLogs("worker-1", "xmrig-0", 100) func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, error) { - identity := c.node.Identity() + identity := c.nodeManager.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -241,13 +241,13 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, return nil, core.E("Controller.RemoteLogs", "failed to create message", err) } - resp, err := c.sendRequest(peerID, msg, 10*time.Second) + response, err := c.sendRequest(peerID, msg, 10*time.Second) if err != nil { return nil, err } var logs LogsPayload - if err := ParseResponse(resp, MessageLogs, &logs); err != nil { + if err := ParseResponse(response, MessageLogs, &logs); err != nil { return nil, err } @@ -262,7 +262,7 @@ func (c *Controller) AllStats() map[string]*StatsPayload { var mu sync.Mutex var wg sync.WaitGroup - for peer := range c.peers.ConnectedPeers() { + for peer := range c.peerRegistry.ConnectedPeers() { wg.Add(1) go func(p *Peer) { defer wg.Done() @@ -289,7 +289,7 @@ func (c *Controller) AllStats() map[string]*StatsPayload { // // rttMS, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { - identity := c.node.Identity() + identity := c.nodeManager.Identity() if identity == nil { return 0, ErrorIdentityNotInitialized } @@ -304,12 +304,12 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return 0, core.E("Controller.PingPeer", "failed to create message", err) } - resp, err := c.sendRequest(peerID, msg, 5*time.Second) + response, err := c.sendRequest(peerID, msg, 5*time.Second) if err != nil { return 0, err } - if err := ValidateResponse(resp, MessagePong); err != nil { + if err := ValidateResponse(response, MessagePong); err != nil { return 0, err } @@ -317,9 +317,9 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { rtt := time.Since(sentAt).Seconds() * 1000 // Convert to ms // Update peer metrics - peer := c.peers.Peer(peerID) + peer := c.peerRegistry.Peer(peerID) if peer != nil { - c.peers.UpdateMetrics(peerID, rtt, peer.GeoKM, peer.Hops) + c.peerRegistry.UpdateMetrics(peerID, rtt, peer.GeoKM, peer.Hops) } return rtt, nil @@ -329,7 +329,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { // // err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { - peer := c.peers.Peer(peerID) + peer := c.peerRegistry.Peer(peerID) if peer == nil { return core.E("Controller.ConnectToPeer", "peer not found: "+peerID, nil) } diff --git a/node/controller_test.go b/node/controller_test.go index b4a670c..840ec3f 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -295,9 +295,9 @@ func TestController_DeadPeerCleanup_Good(t *testing.T) { // The defer block inside sendRequest should have cleaned up the pending entry. time.Sleep(50 * time.Millisecond) - controller.mu.RLock() - pendingCount := len(controller.pending) - controller.mu.RUnlock() + controller.mutex.RLock() + pendingCount := len(controller.pendingRequests) + controller.mutex.RUnlock() assert.Equal(t, 0, pendingCount, "pending map should be empty after timeout — no goroutine/memory leak") @@ -683,9 +683,9 @@ func TestController_HandleResponse_NonReply_Good(t *testing.T) { controller.handleResponse(nil, msg) // No pending entries should be affected - controller.mu.RLock() - count := len(controller.pending) - controller.mu.RUnlock() + controller.mutex.RLock() + count := len(controller.pendingRequests) + controller.mutex.RUnlock() assert.Equal(t, 0, count) } @@ -697,9 +697,9 @@ func TestController_HandleResponse_FullChannel_Ugly(t *testing.T) { ch := make(chan *Message, 1) ch <- &Message{} // Fill the channel - controller.mu.Lock() - controller.pending["test-id"] = ch - controller.mu.Unlock() + controller.mutex.Lock() + controller.pendingRequests["test-id"] = ch + controller.mutex.Unlock() // handleResponse with matching reply should not panic on full channel msg, _ := NewMessage(MessagePong, "sender", "target", PongPayload{SentAt: 123}) @@ -707,9 +707,9 @@ func TestController_HandleResponse_FullChannel_Ugly(t *testing.T) { controller.handleResponse(nil, msg) // The pending entry should be removed despite channel being full - controller.mu.RLock() - _, exists := controller.pending["test-id"] - controller.mu.RUnlock() + controller.mutex.RLock() + _, exists := controller.pendingRequests["test-id"] + controller.mutex.RUnlock() assert.False(t, exists, "pending entry should be removed after handling") } diff --git a/node/worker.go b/node/worker.go index ae26bcb..4b9b4ea 100644 --- a/node/worker.go +++ b/node/worker.go @@ -43,23 +43,23 @@ type ProfileManager interface { // // worker := NewWorker(nodeManager, transport) type Worker struct { - node *NodeManager + nodeManager *NodeManager transport *Transport minerManager MinerManager profileManager ProfileManager - startTime time.Time - DataDir string // Base directory for deployments (defaults to xdg.DataHome) + startedAt time.Time + DataDirectory string // Base directory for deployments (defaults to xdg.DataHome) } // NewWorker creates a new Worker instance. // // worker := NewWorker(nodeManager, transport) -func NewWorker(node *NodeManager, transport *Transport) *Worker { +func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { return &Worker{ - node: node, - transport: transport, - startTime: time.Now(), - DataDir: xdg.DataHome, + nodeManager: nodeManager, + transport: transport, + startedAt: time.Now(), + DataDirectory: xdg.DataHome, } } @@ -104,7 +104,7 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { if err != nil { // Send error response - identity := w.node.Identity() + identity := w.nodeManager.Identity() if identity != nil { errMsg, _ := NewErrorMessage( identity.ID, @@ -145,7 +145,7 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { // handleStats responds with current miner statistics. func (w *Worker) handleStats(msg *Message) (*Message, error) { - identity := w.node.Identity() + identity := w.nodeManager.Identity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -154,7 +154,7 @@ func (w *Worker) handleStats(msg *Message) (*Message, error) { NodeID: identity.ID, NodeName: identity.Name, Miners: []MinerStatsItem{}, - Uptime: int64(time.Since(w.startTime).Seconds()), + Uptime: int64(time.Since(w.startedAt).Seconds()), } if w.minerManager != nil { @@ -366,8 +366,8 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err case BundleMiner, BundleFull: // Determine installation directory - // We use w.DataDir/lethean-desktop/miners/ - minersDir := core.JoinPath(w.DataDir, "lethean-desktop", "miners") + // We use w.DataDirectory/lethean-desktop/miners/ + minersDir := core.JoinPath(w.DataDirectory, "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) logging.Info("deploying miner bundle", logging.Fields{ diff --git a/node/worker_test.go b/node/worker_test.go index 122224c..f5f5817 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -35,13 +35,13 @@ func TestWorker_NewWorker_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() if worker == nil { t.Fatal("NewWorker returned nil") } - if worker.node != nm { - t.Error("worker.node not set correctly") + if worker.nodeManager != nm { + t.Error("worker.nodeManager not set correctly") } if worker.transport != transport { t.Error("worker.transport not set correctly") @@ -68,7 +68,7 @@ func TestWorker_SetMinerManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() mockManager := &mockMinerManager{} worker.SetMinerManager(mockManager) @@ -98,7 +98,7 @@ func TestWorker_SetProfileManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() mockProfile := &mockProfileManager{} worker.SetProfileManager(mockProfile) @@ -128,7 +128,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a ping message identity := nm.Identity() @@ -189,7 +189,7 @@ func TestWorker_HandleStats_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a stats request message. identity := nm.Identity() @@ -249,7 +249,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a start_miner message identity := nm.Identity() @@ -289,7 +289,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a stop_miner message identity := nm.Identity() @@ -329,7 +329,7 @@ func TestWorker_HandleLogs_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a logs request message. identity := nm.Identity() @@ -369,7 +369,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a deploy message for profile identity := nm.Identity() @@ -413,7 +413,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Create a deploy message with unknown type identity := nm.Identity() @@ -596,7 +596,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() mm := &mockMinerManager{ miners: []MinerInstance{}, @@ -763,7 +763,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() t.Run("Success", func(t *testing.T) { @@ -827,7 +827,7 @@ func TestWorker_HandleLogs_WithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() t.Run("Success", func(t *testing.T) { @@ -934,7 +934,7 @@ func TestWorker_HandleStats_WithMinerManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() // Set miner manager with miners that have real stats @@ -995,7 +995,7 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() msg, _ := NewMessage("unknown_type", "sender-id", identity.ID, nil) @@ -1022,7 +1022,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) @@ -1077,7 +1077,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() worker.SetProfileManager(&mockProfileManagerFailing{}) identity := nm.Identity() @@ -1123,7 +1123,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) @@ -1187,7 +1187,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() @@ -1243,7 +1243,7 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() // Set a failing profile manager to exercise the warn-and-continue path worker.SetProfileManager(&mockProfileManagerFailing{}) @@ -1296,7 +1296,7 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() identity := nm.Identity() // Create a message with invalid payload @@ -1321,7 +1321,7 @@ func TestWorker_HandleStats_NoIdentity_Bad(t *testing.T) { pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDir = t.TempDir() + worker.DataDirectory = t.TempDir() msg, _ := NewMessage(MessageGetStats, "sender-id", "target-id", nil) _, err := worker.handleStats(msg) -- 2.45.3 From 885070d24105eda00d357a4bd238d153d6efb77f Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:46:11 +0000 Subject: [PATCH 25/60] refactor(node): adopt AX naming across core APIs Co-Authored-By: Virgil --- node/controller.go | 59 ++++++++++++++++++++++++--------------- node/controller_test.go | 21 ++++++++++---- node/identity.go | 16 +++++++++-- node/peer.go | 62 ++++++++++++++++++++++++++++++----------- node/protocol.go | 11 ++++++-- node/transport.go | 44 ++++++++++++++++++----------- node/worker.go | 59 +++++++++++++++++++++++++++------------ node/worker_test.go | 48 +++++++++++++++++++++++-------- 8 files changed, 223 insertions(+), 97 deletions(-) diff --git a/node/controller.go b/node/controller.go index 07a62ae..3a72e4c 100644 --- a/node/controller.go +++ b/node/controller.go @@ -72,8 +72,8 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat resolvedPeerID := peerID // Auto-connect if not already connected - if c.transport.Connection(peerID) == nil { - peer := c.peerRegistry.Peer(peerID) + if c.transport.GetConnection(peerID) == nil { + peer := c.peerRegistry.GetPeer(peerID) if peer == nil { return nil, core.E("Controller.sendRequest", "peer not found: "+peerID, nil) } @@ -119,18 +119,18 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat } } -// RemoteStats requests miner statistics from a remote peer. +// GetRemoteStats requests miner statistics from a remote peer. // -// stats, err := controller.RemoteStats("worker-1") -func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { - identity := c.nodeManager.Identity() +// stats, err := controller.GetRemoteStats("worker-1") +func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { + identity := c.nodeManager.GetIdentity() if identity == nil { return nil, ErrorIdentityNotInitialized } msg, err := NewMessage(MessageGetStats, identity.ID, peerID, nil) if err != nil { - return nil, core.E("Controller.RemoteStats", "failed to create message", err) + return nil, core.E("Controller.GetRemoteStats", "failed to create message", err) } response, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -146,11 +146,16 @@ func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } +// RemoteStats is a deprecated compatibility alias for GetRemoteStats. +func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { + return c.GetRemoteStats(peerID) +} + // StartRemoteMiner requests a remote peer to start a miner with a given profile. // // err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { - identity := c.nodeManager.Identity() + identity := c.nodeManager.GetIdentity() if identity == nil { return ErrorIdentityNotInitialized } @@ -191,7 +196,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi // // err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { - identity := c.nodeManager.Identity() + identity := c.nodeManager.GetIdentity() if identity == nil { return ErrorIdentityNotInitialized } @@ -222,11 +227,11 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return nil } -// RemoteLogs requests console logs from a remote miner. +// GetRemoteLogs requests console logs from a remote miner. // -// logs, err := controller.RemoteLogs("worker-1", "xmrig-0", 100) -func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, error) { - identity := c.nodeManager.Identity() +// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) +func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { + identity := c.nodeManager.GetIdentity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -238,7 +243,7 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, msg, err := NewMessage(MessageGetLogs, identity.ID, peerID, payload) if err != nil { - return nil, core.E("Controller.RemoteLogs", "failed to create message", err) + return nil, core.E("Controller.GetRemoteLogs", "failed to create message", err) } response, err := c.sendRequest(peerID, msg, 10*time.Second) @@ -254,10 +259,15 @@ func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, return logs.Lines, nil } -// AllStats fetches stats from all connected peers. +// RemoteLogs is a deprecated compatibility alias for GetRemoteLogs. +func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, error) { + return c.GetRemoteLogs(peerID, minerName, lines) +} + +// GetAllStats fetches stats from all connected peers. // -// statsByPeerID := controller.AllStats() -func (c *Controller) AllStats() map[string]*StatsPayload { +// statsByPeerID := controller.GetAllStats() +func (c *Controller) GetAllStats() map[string]*StatsPayload { results := make(map[string]*StatsPayload) var mu sync.Mutex var wg sync.WaitGroup @@ -266,7 +276,7 @@ func (c *Controller) AllStats() map[string]*StatsPayload { wg.Add(1) go func(p *Peer) { defer wg.Done() - stats, err := c.RemoteStats(p.ID) + stats, err := c.GetRemoteStats(p.ID) if err != nil { logging.Debug("failed to get stats from peer", logging.Fields{ "peer_id": p.ID, @@ -285,11 +295,16 @@ func (c *Controller) AllStats() map[string]*StatsPayload { return results } +// AllStats is a deprecated compatibility alias for GetAllStats. +func (c *Controller) AllStats() map[string]*StatsPayload { + return c.GetAllStats() +} + // PingPeer sends a ping to a peer and refreshes that peer's metrics. // // rttMS, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { - identity := c.nodeManager.Identity() + identity := c.nodeManager.GetIdentity() if identity == nil { return 0, ErrorIdentityNotInitialized } @@ -317,7 +332,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { rtt := time.Since(sentAt).Seconds() * 1000 // Convert to ms // Update peer metrics - peer := c.peerRegistry.Peer(peerID) + peer := c.peerRegistry.GetPeer(peerID) if peer != nil { c.peerRegistry.UpdateMetrics(peerID, rtt, peer.GeoKM, peer.Hops) } @@ -329,7 +344,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { // // err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { - peer := c.peerRegistry.Peer(peerID) + peer := c.peerRegistry.GetPeer(peerID) if peer == nil { return core.E("Controller.ConnectToPeer", "peer not found: "+peerID, nil) } @@ -342,7 +357,7 @@ func (c *Controller) ConnectToPeer(peerID string) error { // // err := controller.DisconnectFromPeer("worker-1") func (c *Controller) DisconnectFromPeer(peerID string) error { - conn := c.transport.Connection(peerID) + conn := c.transport.GetConnection(peerID) if conn == nil { return core.E("Controller.DisconnectFromPeer", "peer not connected: "+peerID, nil) } diff --git a/node/controller_test.go b/node/controller_test.go index 840ec3f..8a38c84 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -490,17 +490,21 @@ func (m *mockMinerManagerFull) ListMiners() []MinerInstance { return result } -func (m *mockMinerManagerFull) Miner(name string) (MinerInstance, error) { +func (m *mockMinerManagerFull) GetMiner(name string) (MinerInstance, error) { m.mu.Lock() defer m.mu.Unlock() miner, exists := m.miners[name] if !exists { - return nil, core.E("mockMinerManagerFull.Miner", "miner "+name+" not found", nil) + return nil, core.E("mockMinerManagerFull.GetMiner", "miner "+name+" not found", nil) } return miner, nil } +func (m *mockMinerManagerFull) Miner(name string) (MinerInstance, error) { + return m.GetMiner(name) +} + // mockMinerFull implements MinerInstance with real data. type mockMinerFull struct { name string @@ -509,16 +513,21 @@ type mockMinerFull struct { consoleHistory []string } -func (m *mockMinerFull) Name() string { return m.name } -func (m *mockMinerFull) Type() string { return m.minerType } -func (m *mockMinerFull) Stats() (any, error) { return m.stats, nil } -func (m *mockMinerFull) ConsoleHistory(lines int) []string { +func (m *mockMinerFull) GetName() string { return m.name } +func (m *mockMinerFull) GetType() string { return m.minerType } +func (m *mockMinerFull) GetStats() (any, error) { return m.stats, nil } +func (m *mockMinerFull) GetConsoleHistory(lines int) []string { if lines >= len(m.consoleHistory) { return m.consoleHistory } return m.consoleHistory[:lines] } +func (m *mockMinerFull) Name() string { return m.GetName() } +func (m *mockMinerFull) Type() string { return m.GetType() } +func (m *mockMinerFull) Stats() (any, error) { return m.GetStats() } +func (m *mockMinerFull) ConsoleHistory(lines int) []string { return m.GetConsoleHistory(lines) } + func TestController_StartRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) serverID := tp.ServerNode.Identity().ID diff --git a/node/identity.go b/node/identity.go index 9a285c3..219fabe 100644 --- a/node/identity.go +++ b/node/identity.go @@ -126,6 +126,11 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } +// NewNodeManagerWithPaths is a deprecated compatibility alias for NewNodeManagerFromPaths. +func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { + return NewNodeManagerFromPaths(keyPath, configPath) +} + // HasIdentity returns true if a node identity has been initialized. func (n *NodeManager) HasIdentity() bool { n.mu.RLock() @@ -133,10 +138,10 @@ func (n *NodeManager) HasIdentity() bool { return n.identity != nil } -// Identity returns a copy of the loaded node identity. +// GetIdentity returns a copy of the loaded node identity. // -// identity := nodeManager.Identity() -func (n *NodeManager) Identity() *NodeIdentity { +// identity := nodeManager.GetIdentity() +func (n *NodeManager) GetIdentity() *NodeIdentity { n.mu.RLock() defer n.mu.RUnlock() if n.identity == nil { @@ -147,6 +152,11 @@ func (n *NodeManager) Identity() *NodeIdentity { return &identity } +// Identity is a deprecated compatibility alias for GetIdentity. +func (n *NodeManager) Identity() *NodeIdentity { + return n.GetIdentity() +} + // GenerateIdentity writes a new node identity for the given name and role. // // err := nodeManager.GenerateIdentity("worker-1", RoleWorker) diff --git a/node/peer.go b/node/peer.go index c43ea23..2ba1420 100644 --- a/node/peer.go +++ b/node/peer.go @@ -160,6 +160,11 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } +// NewPeerRegistryWithPath is a deprecated compatibility alias for NewPeerRegistryFromPath. +func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { + return NewPeerRegistryFromPath(peersPath) +} + // SetAuthMode changes how unknown peers are handled. // // registry.SetAuthMode(PeerAuthAllowlist) @@ -170,15 +175,20 @@ func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { logging.Info("peer auth mode changed", logging.Fields{"mode": mode}) } -// AuthMode returns the current authentication mode. +// GetAuthMode returns the current authentication mode. // -// mode := registry.AuthMode() -func (r *PeerRegistry) AuthMode() PeerAuthMode { +// mode := registry.GetAuthMode() +func (r *PeerRegistry) GetAuthMode() PeerAuthMode { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.authMode } +// AuthMode is a deprecated compatibility alias for GetAuthMode. +func (r *PeerRegistry) AuthMode() PeerAuthMode { + return r.GetAuthMode() +} + // AllowPublicKey adds a public key to the allowlist. func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() @@ -323,10 +333,10 @@ func (r *PeerRegistry) RemovePeer(id string) error { return nil } -// Peer returns a copy of the peer with the supplied ID. +// GetPeer returns a copy of the peer with the supplied ID. // -// peer := registry.Peer("worker-1") -func (r *PeerRegistry) Peer(id string) *Peer { +// peer := registry.GetPeer("worker-1") +func (r *PeerRegistry) GetPeer(id string) *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -340,6 +350,11 @@ func (r *PeerRegistry) Peer(id string) *Peer { return &peerCopy } +// Peer is a deprecated compatibility alias for GetPeer. +func (r *PeerRegistry) Peer(id string) *Peer { + return r.GetPeer(id) +} + // ListPeers returns all registered peers. func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) @@ -406,10 +421,10 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { return nil } -// MarkConnected updates a peer's connection state. +// SetConnected updates a peer's connection state. // -// registry.MarkConnected("worker-1", true) -func (r *PeerRegistry) MarkConnected(id string, connected bool) { +// registry.SetConnected("worker-1", true) +func (r *PeerRegistry) SetConnected(id string, connected bool) { r.mu.Lock() defer r.mu.Unlock() @@ -421,6 +436,11 @@ func (r *PeerRegistry) MarkConnected(id string, connected bool) { } } +// MarkConnected is a deprecated compatibility alias for SetConnected. +func (r *PeerRegistry) MarkConnected(id string, connected bool) { + r.SetConnected(id, connected) +} + // Score adjustment constants const ( ScoreSuccessIncrement = 1.0 // Increment for successful interaction @@ -488,10 +508,10 @@ func (r *PeerRegistry) RecordTimeout(id string) { }) } -// PeersSortedByScore returns peers sorted by score, highest first. +// GetPeersByScore returns peers sorted by score, highest first. // -// peers := registry.PeersSortedByScore() -func (r *PeerRegistry) PeersSortedByScore() []*Peer { +// peers := registry.GetPeersByScore() +func (r *PeerRegistry) GetPeersByScore() []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -511,10 +531,15 @@ func (r *PeerRegistry) PeersSortedByScore() []*Peer { return peers } +// PeersSortedByScore is a deprecated compatibility alias for GetPeersByScore. +func (r *PeerRegistry) PeersSortedByScore() []*Peer { + return r.GetPeersByScore() +} + // PeersByScore returns an iterator over peers sorted by score (highest first). func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { - peers := r.PeersSortedByScore() + peers := r.GetPeersByScore() for _, p := range peers { if !yield(p) { return @@ -576,13 +601,18 @@ func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { return peers } -// ConnectedPeerList returns all currently connected peers as a slice. +// GetConnectedPeers returns all currently connected peers as a slice. // -// connectedPeers := registry.ConnectedPeerList() -func (r *PeerRegistry) ConnectedPeerList() []*Peer { +// connectedPeers := registry.GetConnectedPeers() +func (r *PeerRegistry) GetConnectedPeers() []*Peer { return slices.Collect(r.ConnectedPeers()) } +// ConnectedPeerList is a deprecated compatibility alias for GetConnectedPeers. +func (r *PeerRegistry) ConnectedPeerList() []*Peer { + return r.GetConnectedPeers() +} + // ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { diff --git a/node/protocol.go b/node/protocol.go index 9c30b1e..1476294 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -88,12 +88,17 @@ func IsProtocolError(err error) bool { return ok } -// ProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. +// GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. // -// code := ProtocolErrorCode(err) -func ProtocolErrorCode(err error) int { +// code := GetProtocolErrorCode(err) +func GetProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code } return 0 } + +// ProtocolErrorCode is a deprecated compatibility alias for GetProtocolErrorCode. +func ProtocolErrorCode(err error) int { + return GetProtocolErrorCode(err) +} diff --git a/node/transport.go b/node/transport.go index 30ef318..29bfc99 100644 --- a/node/transport.go +++ b/node/transport.go @@ -264,7 +264,7 @@ func agentHeaderToken(value string) string { // agentUserAgent returns a transparent identity string for request headers. func (t *Transport) agentUserAgent() string { - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() if identity == nil { return core.Sprintf("%s proto=%s", agentUserAgentPrefix, ProtocolVersion) } @@ -431,7 +431,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { }) // Update registry - t.peerRegistry.MarkConnected(pc.Peer.ID, true) + t.peerRegistry.SetConnected(pc.Peer.ID, true) // Start read loop t.waitGroup.Add(1) @@ -491,15 +491,20 @@ func (t *Transport) Broadcast(msg *Message) error { return lastErr } -// Connection returns an active connection to a peer. +// GetConnection returns an active connection to a peer. // -// connection := transport.Connection("worker-1") -func (t *Transport) Connection(peerID string) *PeerConnection { +// connection := transport.GetConnection("worker-1") +func (t *Transport) GetConnection(peerID string) *PeerConnection { t.mutex.RLock() defer t.mutex.RUnlock() return t.connections[peerID] } +// Connection is a deprecated compatibility alias for GetConnection. +func (t *Transport) Connection(peerID string) *PeerConnection { + return t.GetConnection(peerID) +} + // handleWSUpgrade handles incoming WebSocket connections. func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -569,7 +574,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "peer_id": payload.Identity.ID, "user_agent": userAgent, }) - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -601,7 +606,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { "user_agent": userAgent, }) // Send rejection before closing - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() if identity != nil { rejectPayload := HandshakeAckPayload{ Identity: *identity, @@ -618,7 +623,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Create peer if not exists (only if auth passed) - peer := t.peerRegistry.Peer(payload.Identity.ID) + peer := t.peerRegistry.GetPeer(payload.Identity.ID) if peer == nil { // Auto-register the peer since they passed allowlist check peer = &Peer{ @@ -647,7 +652,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { } // Send handshake acknowledgment - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() if identity == nil { conn.Close() return @@ -689,7 +694,7 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { t.mutex.Unlock() // Update registry - t.peerRegistry.MarkConnected(peer.ID, true) + t.peerRegistry.SetConnected(peer.ID, true) logging.Debug("accepted peer connection", logging.Fields{ "peer_id": peer.ID, @@ -718,7 +723,7 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { pc.Conn.SetReadDeadline(time.Time{}) }() - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() if identity == nil { return ErrorIdentityNotInitialized } @@ -900,7 +905,7 @@ func (t *Transport) keepalive(pc *PeerConnection) { } // Send ping - identity := t.nodeManager.Identity() + identity := t.nodeManager.GetIdentity() pingMsg, err := NewMessage(MessagePing, identity.ID, pc.Peer.ID, PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -922,7 +927,7 @@ func (t *Transport) removeConnection(pc *PeerConnection) { delete(t.connections, pc.Peer.ID) t.mutex.Unlock() - t.peerRegistry.MarkConnected(pc.Peer.ID, false) + t.peerRegistry.SetConnected(pc.Peer.ID, false) pc.Close() } @@ -981,7 +986,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { // already manages write deadlines under the lock. Setting it here // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { - identity := pc.transport.nodeManager.Identity() + identity := pc.transport.nodeManager.GetIdentity() if identity != nil { payload := DisconnectPayload{ Reason: reason, @@ -1039,11 +1044,16 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeerCount returns the number of connected peers. +// ConnectedPeers returns the number of connected peers. // -// count := transport.ConnectedPeerCount() -func (t *Transport) ConnectedPeerCount() int { +// count := transport.ConnectedPeers() +func (t *Transport) ConnectedPeers() int { t.mutex.RLock() defer t.mutex.RUnlock() return len(t.connections) } + +// ConnectedPeerCount is a deprecated compatibility alias for ConnectedPeers. +func (t *Transport) ConnectedPeerCount() int { + return t.ConnectedPeers() +} diff --git a/node/worker.go b/node/worker.go index 4b9b4ea..e29e5aa 100644 --- a/node/worker.go +++ b/node/worker.go @@ -18,24 +18,24 @@ type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error ListMiners() []MinerInstance - Miner(name string) (MinerInstance, error) + GetMiner(name string) (MinerInstance, error) } // MinerInstance represents a running miner for stats collection. // // var miner MinerInstance type MinerInstance interface { - Name() string - Type() string - Stats() (any, error) - ConsoleHistory(lines int) []string + GetName() string + GetType() string + GetStats() (any, error) + GetConsoleHistory(lines int) []string } // ProfileManager interface for profile operations. // // var profileManager ProfileManager type ProfileManager interface { - Profile(id string) (any, error) + GetProfile(id string) (any, error) SaveProfile(profile any) error } @@ -48,7 +48,8 @@ type Worker struct { minerManager MinerManager profileManager ProfileManager startedAt time.Time - DataDirectory string // Base directory for deployments (defaults to xdg.DataHome) + DataDir string // Base directory for deployments (defaults to xdg.DataHome) + DataDirectory string // Deprecated compatibility alias for DataDir } // NewWorker creates a new Worker instance. @@ -59,6 +60,7 @@ func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { nodeManager: nodeManager, transport: transport, startedAt: time.Now(), + DataDir: xdg.DataHome, DataDirectory: xdg.DataHome, } } @@ -104,7 +106,7 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { if err != nil { // Send error response - identity := w.nodeManager.Identity() + identity := w.nodeManager.GetIdentity() if identity != nil { errMsg, _ := NewErrorMessage( identity.ID, @@ -145,7 +147,7 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { // handleStats responds with current miner statistics. func (w *Worker) handleStats(msg *Message) (*Message, error) { - identity := w.nodeManager.Identity() + identity := w.nodeManager.GetIdentity() if identity == nil { return nil, ErrorIdentityNotInitialized } @@ -160,7 +162,7 @@ func (w *Worker) handleStats(msg *Message) (*Message, error) { if w.minerManager != nil { miners := w.minerManager.ListMiners() for _, miner := range miners { - minerStats, err := miner.Stats() + minerStats, err := miner.GetStats() if err != nil { continue } @@ -178,8 +180,8 @@ func (w *Worker) handleStats(msg *Message) (*Message, error) { // convertMinerStats converts miner stats to the protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ - Name: miner.Name(), - Type: miner.Type(), + Name: miner.GetName(), + Type: miner.GetType(), } // Try to extract common fields from the stats @@ -228,7 +230,7 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { if payload.Config != nil { config = payload.Config } else if w.profileManager != nil { - profile, err := w.profileManager.Profile(payload.ProfileID) + profile, err := w.profileManager.GetProfile(payload.ProfileID) if err != nil { return nil, core.E("Worker.handleStartMiner", "profile not found: "+payload.ProfileID, nil) } @@ -249,7 +251,7 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { ack := MinerAckPayload{ Success: true, - MinerName: miner.Name(), + MinerName: miner.GetName(), } return msg.Reply(MessageMinerAck, ack) } @@ -294,12 +296,12 @@ func (w *Worker) handleLogs(msg *Message) (*Message, error) { payload.Lines = maxLogLines } - miner, err := w.minerManager.Miner(payload.MinerName) + miner, err := w.minerManager.GetMiner(payload.MinerName) if err != nil { return nil, core.E("Worker.handleLogs", "miner not found: "+payload.MinerName, nil) } - lines := miner.ConsoleHistory(payload.Lines) + lines := miner.GetConsoleHistory(payload.Lines) logs := LogsPayload{ MinerName: payload.MinerName, @@ -366,8 +368,9 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err case BundleMiner, BundleFull: // Determine installation directory - // We use w.DataDirectory/lethean-desktop/miners/ - minersDir := core.JoinPath(w.DataDirectory, "lethean-desktop", "miners") + // We use the configured deployment directory for + // lethean-desktop/miners/. + minersDir := core.JoinPath(w.deploymentDir(), "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) logging.Info("deploying miner bundle", logging.Fields{ @@ -419,3 +422,23 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } + +// RegisterWithTransport is a deprecated compatibility alias for RegisterOnTransport. +func (w *Worker) RegisterWithTransport() { + w.RegisterOnTransport() +} + +// deploymentDir resolves the active deployment directory, preferring DataDir +// unless a caller has only populated the legacy DataDirectory field. +func (w *Worker) deploymentDir() string { + switch { + case w.DataDir != "" && w.DataDir != xdg.DataHome: + return w.DataDir + case w.DataDirectory != "": + return w.DataDirectory + case w.DataDir != "": + return w.DataDir + default: + return xdg.DataHome + } +} diff --git a/node/worker_test.go b/node/worker_test.go index f5f5817..4fc8265 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -499,32 +499,44 @@ func (m *mockMinerManager) ListMiners() []MinerInstance { return m.miners } -func (m *mockMinerManager) Miner(name string) (MinerInstance, error) { +func (m *mockMinerManager) GetMiner(name string) (MinerInstance, error) { for _, miner := range m.miners { - if miner.Name() == name { + if miner.GetName() == name { return miner, nil } } return nil, nil } +func (m *mockMinerManager) Miner(name string) (MinerInstance, error) { + return m.GetMiner(name) +} + type mockMinerInstance struct { name string minerType string stats any } -func (m *mockMinerInstance) Name() string { return m.name } -func (m *mockMinerInstance) Type() string { return m.minerType } -func (m *mockMinerInstance) Stats() (any, error) { return m.stats, nil } -func (m *mockMinerInstance) ConsoleHistory(lines int) []string { return []string{} } +func (m *mockMinerInstance) GetName() string { return m.name } +func (m *mockMinerInstance) GetType() string { return m.minerType } +func (m *mockMinerInstance) GetStats() (any, error) { return m.stats, nil } +func (m *mockMinerInstance) GetConsoleHistory(lines int) []string { return []string{} } +func (m *mockMinerInstance) Name() string { return m.GetName() } +func (m *mockMinerInstance) Type() string { return m.GetType() } +func (m *mockMinerInstance) Stats() (any, error) { return m.GetStats() } +func (m *mockMinerInstance) ConsoleHistory(lines int) []string { return m.GetConsoleHistory(lines) } type mockProfileManager struct{} -func (m *mockProfileManager) Profile(id string) (any, error) { +func (m *mockProfileManager) GetProfile(id string) (any, error) { return nil, nil } +func (m *mockProfileManager) Profile(id string) (any, error) { + return m.GetProfile(id) +} + func (m *mockProfileManager) SaveProfile(profile any) error { return nil } @@ -544,19 +556,23 @@ func (m *mockMinerManagerFailing) StopMiner(name string) error { return core.E("mockMinerManagerFailing.StopMiner", "miner "+name+" not found", nil) } -func (m *mockMinerManagerFailing) Miner(name string) (MinerInstance, error) { +func (m *mockMinerManagerFailing) GetMiner(name string) (MinerInstance, error) { return nil, core.E("mockMinerManagerFailing.Miner", "miner "+name+" not found", nil) } +func (m *mockMinerManagerFailing) Miner(name string) (MinerInstance, error) { + return m.GetMiner(name) +} + // mockProfileManagerFull implements ProfileManager that returns real data. type mockProfileManagerFull struct { profiles map[string]any } -func (m *mockProfileManagerFull) Profile(id string) (any, error) { +func (m *mockProfileManagerFull) GetProfile(id string) (any, error) { p, ok := m.profiles[id] if !ok { - return nil, core.E("mockProfileManagerFull.Profile", "profile "+id+" not found", nil) + return nil, core.E("mockProfileManagerFull.GetProfile", "profile "+id+" not found", nil) } return p, nil } @@ -565,17 +581,25 @@ func (m *mockProfileManagerFull) SaveProfile(profile any) error { return nil } +func (m *mockProfileManagerFull) Profile(id string) (any, error) { + return m.GetProfile(id) +} + // mockProfileManagerFailing always returns errors. type mockProfileManagerFailing struct{} -func (m *mockProfileManagerFailing) Profile(id string) (any, error) { - return nil, core.E("mockProfileManagerFailing.Profile", "profile "+id+" not found", nil) +func (m *mockProfileManagerFailing) GetProfile(id string) (any, error) { + return nil, core.E("mockProfileManagerFailing.GetProfile", "profile "+id+" not found", nil) } func (m *mockProfileManagerFailing) SaveProfile(profile any) error { return core.E("mockProfileManagerFailing.SaveProfile", "save failed", nil) } +func (m *mockProfileManagerFailing) Profile(id string) (any, error) { + return m.GetProfile(id) +} + func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() -- 2.45.3 From bd9efd1deb886fcd64152687af99b091d428ef5b Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 22:54:45 +0000 Subject: [PATCH 26/60] refactor(node): clarify AX-facing transport and worker names Co-Authored-By: Virgil --- node/controller_test.go | 2 +- node/integration_test.go | 2 +- node/transport.go | 78 ++++++++++++++++++++++++++++++++++------ node/transport_test.go | 16 ++++----- node/worker.go | 49 ++++++++++++++----------- node/worker_test.go | 50 +++++++++++++------------- 6 files changed, 130 insertions(+), 67 deletions(-) diff --git a/node/controller_test.go b/node/controller_test.go index 8a38c84..03b121e 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -50,7 +50,7 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { srv := NewTransport(nm, reg, cfg) mux := http.NewServeMux() - mux.HandleFunc(cfg.WSPath, srv.handleWSUpgrade) + mux.HandleFunc(cfg.WebSocketPath, srv.handleWSUpgrade) ts := httptest.NewServer(mux) u, _ := url.Parse(ts.URL) diff --git a/node/integration_test.go b/node/integration_test.go index 96de5d2..9eadf12 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -82,7 +82,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // Start the worker transport behind httptest. mux := http.NewServeMux() - mux.HandleFunc(workerCfg.WSPath, workerTransport.handleWSUpgrade) + mux.HandleFunc(workerCfg.WebSocketPath, workerTransport.handleWSUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { controllerTransport.Stop() diff --git a/node/transport.go b/node/transport.go index 29bfc99..b0a72fa 100644 --- a/node/transport.go +++ b/node/transport.go @@ -33,15 +33,23 @@ const DefaultMaxMessageSize int64 = 1 << 20 // 1MB // agentUserAgentPrefix identifies this tool in request headers. const agentUserAgentPrefix = "agent-go-p2p" +const ( + defaultTransportListenAddress = ":9091" + defaultTransportWebSocketPath = "/ws" + defaultTransportMaximumConnections = 100 +) + // TransportConfig configures the WebSocket transport. // // cfg := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default - WSPath string // "/ws" - WebSocket endpoint path + WebSocketPath string // "/ws" - WebSocket endpoint path + WSPath string // Deprecated compatibility alias for WebSocketPath. TLSCertPath string // Optional TLS for wss:// TLSKeyPath string - MaxConns int // Maximum concurrent connections + MaxConnections int // Maximum concurrent connections + MaxConns int // Deprecated compatibility alias for MaxConnections. MaxMessageSize int64 // Maximum message size in bytes (0 = 1MB default) PingInterval time.Duration // WebSocket keepalive interval PongTimeout time.Duration // Timeout waiting for pong @@ -52,15 +60,61 @@ type TransportConfig struct { // cfg := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ - ListenAddr: ":9091", - WSPath: "/ws", - MaxConns: 100, + ListenAddr: defaultTransportListenAddress, + WebSocketPath: defaultTransportWebSocketPath, + WSPath: defaultTransportWebSocketPath, + MaxConnections: defaultTransportMaximumConnections, + MaxConns: defaultTransportMaximumConnections, MaxMessageSize: DefaultMaxMessageSize, PingInterval: 30 * time.Second, PongTimeout: 10 * time.Second, } } +// listenAddress returns the effective listen address, falling back to the +// default when the config leaves it empty. +func (c TransportConfig) listenAddress() string { + if c.ListenAddr != "" { + return c.ListenAddr + } + return defaultTransportListenAddress +} + +// webSocketPath returns the effective WebSocket endpoint path, preferring the +// clearer WebSocketPath field and falling back to the compatibility alias. +func (c TransportConfig) webSocketPath() string { + switch { + case c.WebSocketPath != "" && c.WebSocketPath != defaultTransportWebSocketPath: + return c.WebSocketPath + case c.WSPath != "" && c.WSPath != defaultTransportWebSocketPath: + return c.WSPath + case c.WebSocketPath != "": + return c.WebSocketPath + case c.WSPath != "": + return c.WSPath + default: + return defaultTransportWebSocketPath + } +} + +// maximumConnections returns the effective concurrent connection limit, +// preferring the clearer MaxConnections field and falling back to the +// compatibility alias. +func (c TransportConfig) maximumConnections() int { + switch { + case c.MaxConnections != 0 && c.MaxConnections != defaultTransportMaximumConnections: + return c.MaxConnections + case c.MaxConns != 0 && c.MaxConns != defaultTransportMaximumConnections: + return c.MaxConns + case c.MaxConnections != 0: + return c.MaxConnections + case c.MaxConns != 0: + return c.MaxConns + default: + return defaultTransportMaximumConnections + } +} + // MessageHandler processes incoming messages. // // var handler MessageHandler = func(conn *PeerConnection, msg *Message) {} @@ -282,10 +336,12 @@ func (t *Transport) agentUserAgent() string { // Start opens the WebSocket listener and background maintenance loops. func (t *Transport) Start() error { mux := http.NewServeMux() - mux.HandleFunc(t.config.WSPath, t.handleWSUpgrade) + mux.HandleFunc(t.config.webSocketPath(), t.handleWSUpgrade) + + listenAddress := t.config.listenAddress() t.httpServer = &http.Server{ - Addr: t.config.ListenAddr, + Addr: listenAddress, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -325,7 +381,7 @@ func (t *Transport) Start() error { err = t.httpServer.ListenAndServe() } if err != nil && err != http.ErrServerClosed { - logging.Error("HTTP server error", logging.Fields{"error": err, "addr": t.config.ListenAddr}) + logging.Error("HTTP server error", logging.Fields{"error": err, "addr": listenAddress}) } }) @@ -389,7 +445,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { if t.config.TLSCertPath != "" { scheme = "wss" } - peerURL := url.URL{Scheme: scheme, Host: peer.Address, Path: t.config.WSPath} + peerURL := url.URL{Scheme: scheme, Host: peer.Address, Path: t.config.webSocketPath()} userAgent := t.agentUserAgent() // Dial the peer with timeout to prevent hanging on unresponsive peers @@ -509,14 +565,14 @@ func (t *Transport) Connection(peerID string) *PeerConnection { func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") - // Enforce MaxConns limit (including pending connections during handshake) + // Enforce the maximum connection limit, including pending handshakes. t.mutex.RLock() currentConnections := len(t.connections) t.mutex.RUnlock() pendingHandshakeCount := int(t.pendingHandshakeCount.Load()) totalConnections := currentConnections + pendingHandshakeCount - if totalConnections >= t.config.MaxConns { + if totalConnections >= t.config.maximumConnections() { http.Error(w, "Too many connections", http.StatusServiceUnavailable) return } diff --git a/node/transport_test.go b/node/transport_test.go index fa90ab0..6416995 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -72,7 +72,7 @@ func setupTestTransportPairWithConfig(t *testing.T, serverCfg, clientCfg Transpo // Use httptest.Server with the transport's WebSocket handler mux := http.NewServeMux() - mux.HandleFunc(serverCfg.WSPath, serverTransport.handleWSUpgrade) + mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWSUpgrade) ts := httptest.NewServer(mux) u, _ := url.Parse(ts.URL) @@ -251,7 +251,7 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { var capturedUserAgent atomic.Value mux := http.NewServeMux() - mux.HandleFunc(serverCfg.WSPath, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(serverCfg.WebSocketPath, func(w http.ResponseWriter, r *http.Request) { capturedUserAgent.Store(r.Header.Get("User-Agent")) serverTransport.handleWSUpgrade(w, r) }) @@ -472,17 +472,17 @@ func TestTransport_RateLimiting_Good(t *testing.T) { } } -func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { - // Server with MaxConns=1 +func TestTransport_MaxConnectionsEnforcement_Good(t *testing.T) { + // Server with MaxConnections=1 serverNM := newTestNodeManager(t, "maxconns-server", RoleWorker) serverReg := newTestPeerRegistry(t) serverCfg := DefaultTransportConfig() - serverCfg.MaxConns = 1 + serverCfg.MaxConnections = 1 serverTransport := NewTransport(serverNM, serverReg, serverCfg) mux := http.NewServeMux() - mux.HandleFunc(serverCfg.WSPath, serverTransport.handleWSUpgrade) + mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWSUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { serverTransport.Stop() @@ -509,7 +509,7 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { // Allow server to register the connection time.Sleep(50 * time.Millisecond) - // Second client should be rejected (MaxConns=1 reached) + // Second client should be rejected (MaxConnections=1 reached) client2NM := newTestNodeManager(t, "client2", RoleController) client2Reg := newTestPeerRegistry(t) client2Transport := NewTransport(client2NM, client2Reg, DefaultTransportConfig()) @@ -520,7 +520,7 @@ func TestTransport_MaxConnsEnforcement_Good(t *testing.T) { _, err = client2Transport.Connect(peer2) if err == nil { - t.Fatal("second connection should be rejected when MaxConns=1") + t.Fatal("second connection should be rejected when MaxConnections=1") } } diff --git a/node/worker.go b/node/worker.go index e29e5aa..dd034db 100644 --- a/node/worker.go +++ b/node/worker.go @@ -43,13 +43,14 @@ type ProfileManager interface { // // worker := NewWorker(nodeManager, transport) type Worker struct { - nodeManager *NodeManager - transport *Transport - minerManager MinerManager - profileManager ProfileManager - startedAt time.Time - DataDir string // Base directory for deployments (defaults to xdg.DataHome) - DataDirectory string // Deprecated compatibility alias for DataDir + nodeManager *NodeManager + transport *Transport + minerManager MinerManager + profileManager ProfileManager + startedAt time.Time + DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) + DataDir string // Deprecated compatibility alias for DeploymentDirectory + DataDirectory string // Deprecated compatibility alias for DeploymentDirectory } // NewWorker creates a new Worker instance. @@ -57,11 +58,12 @@ type Worker struct { // worker := NewWorker(nodeManager, transport) func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { return &Worker{ - nodeManager: nodeManager, - transport: transport, - startedAt: time.Now(), - DataDir: xdg.DataHome, - DataDirectory: xdg.DataHome, + nodeManager: nodeManager, + transport: transport, + startedAt: time.Now(), + DeploymentDirectory: xdg.DataHome, + DataDir: xdg.DataHome, + DataDirectory: xdg.DataHome, } } @@ -367,10 +369,9 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err return msg.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: - // Determine installation directory - // We use the configured deployment directory for - // lethean-desktop/miners/. - minersDir := core.JoinPath(w.deploymentDir(), "lethean-desktop", "miners") + // Determine the installation directory under the configured deployment + // root for lethean-desktop/miners/. + minersDir := core.JoinPath(w.deploymentDirectory(), "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) logging.Info("deploying miner bundle", logging.Fields{ @@ -428,16 +429,22 @@ func (w *Worker) RegisterWithTransport() { w.RegisterOnTransport() } -// deploymentDir resolves the active deployment directory, preferring DataDir -// unless a caller has only populated the legacy DataDirectory field. -func (w *Worker) deploymentDir() string { +// deploymentDirectory resolves the active deployment directory, preferring +// DeploymentDirectory unless a caller has only populated the legacy aliases. +func (w *Worker) deploymentDirectory() string { switch { + case w.DeploymentDirectory != "" && w.DeploymentDirectory != xdg.DataHome: + return w.DeploymentDirectory case w.DataDir != "" && w.DataDir != xdg.DataHome: return w.DataDir + case w.DataDirectory != "" && w.DataDirectory != xdg.DataHome: + return w.DataDirectory + case w.DeploymentDirectory != "": + return w.DeploymentDirectory + case w.DataDir != "": + return w.DataDir case w.DataDirectory != "": return w.DataDirectory - case w.DataDir != "": - return w.DataDir default: return xdg.DataHome } diff --git a/node/worker_test.go b/node/worker_test.go index 4fc8265..24b39fe 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -35,7 +35,7 @@ func TestWorker_NewWorker_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() if worker == nil { t.Fatal("NewWorker returned nil") @@ -68,7 +68,7 @@ func TestWorker_SetMinerManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() mockManager := &mockMinerManager{} worker.SetMinerManager(mockManager) @@ -98,7 +98,7 @@ func TestWorker_SetProfileManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() mockProfile := &mockProfileManager{} worker.SetProfileManager(mockProfile) @@ -128,7 +128,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a ping message identity := nm.Identity() @@ -189,7 +189,7 @@ func TestWorker_HandleStats_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a stats request message. identity := nm.Identity() @@ -249,7 +249,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a start_miner message identity := nm.Identity() @@ -289,7 +289,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a stop_miner message identity := nm.Identity() @@ -329,7 +329,7 @@ func TestWorker_HandleLogs_NoManager_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a logs request message. identity := nm.Identity() @@ -369,7 +369,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a deploy message for profile identity := nm.Identity() @@ -413,7 +413,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Create a deploy message with unknown type identity := nm.Identity() @@ -620,7 +620,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() mm := &mockMinerManager{ miners: []MinerInstance{}, @@ -631,7 +631,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { identity := nm.Identity() - t.Run("WithConfigOverride", func(t *testing.T) { + t.Run("ConfigOverride", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", Config: RawMessage(`{"pool":"test:3333"}`), @@ -678,7 +678,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { } }) - t.Run("WithProfileManager", func(t *testing.T) { + t.Run("ProfileManagerConfigured", func(t *testing.T) { pm := &mockProfileManagerFull{ profiles: map[string]any{ "test-profile": map[string]any{"pool": "pool.test:3333"}, @@ -787,7 +787,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() t.Run("Success", func(t *testing.T) { @@ -851,7 +851,7 @@ func TestWorker_HandleLogs_WithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() t.Run("Success", func(t *testing.T) { @@ -958,7 +958,7 @@ func TestWorker_HandleStats_WithMinerManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() // Set miner manager with miners that have real stats @@ -1019,7 +1019,7 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() msg, _ := NewMessage("unknown_type", "sender-id", identity.ID, nil) @@ -1046,7 +1046,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) @@ -1101,7 +1101,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() worker.SetProfileManager(&mockProfileManagerFailing{}) identity := nm.Identity() @@ -1147,7 +1147,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) @@ -1211,7 +1211,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() @@ -1249,7 +1249,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { } } -func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { +func TestWorker_HandleDeploy_MinerBundle_ProfileManager_Good(t *testing.T) { cleanup := setupTestEnvironment(t) defer cleanup() @@ -1267,7 +1267,7 @@ func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) { } transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() // Set a failing profile manager to exercise the warn-and-continue path worker.SetProfileManager(&mockProfileManagerFailing{}) @@ -1320,7 +1320,7 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() identity := nm.Identity() // Create a message with invalid payload @@ -1345,7 +1345,7 @@ func TestWorker_HandleStats_NoIdentity_Bad(t *testing.T) { pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json")) transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) - worker.DataDirectory = t.TempDir() + worker.DeploymentDirectory = t.TempDir() msg, _ := NewMessage(MessageGetStats, "sender-id", "target-id", nil) _, err := worker.handleStats(msg) -- 2.45.3 From 79591c3635321ec8974dfbb5c2971990858dd10a Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 23:00:36 +0000 Subject: [PATCH 27/60] refactor(node): trim legacy AX compatibility fields Co-Authored-By: Virgil --- node/identity.go | 5 ----- node/peer.go | 5 ----- node/transport.go | 47 ++++++++++++----------------------------------- node/worker.go | 28 +++------------------------- 4 files changed, 15 insertions(+), 70 deletions(-) diff --git a/node/identity.go b/node/identity.go index 219fabe..4acdd74 100644 --- a/node/identity.go +++ b/node/identity.go @@ -126,11 +126,6 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } -// NewNodeManagerWithPaths is a deprecated compatibility alias for NewNodeManagerFromPaths. -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - // HasIdentity returns true if a node identity has been initialized. func (n *NodeManager) HasIdentity() bool { n.mu.RLock() diff --git a/node/peer.go b/node/peer.go index 2ba1420..c17c55b 100644 --- a/node/peer.go +++ b/node/peer.go @@ -160,11 +160,6 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } -// NewPeerRegistryWithPath is a deprecated compatibility alias for NewPeerRegistryFromPath. -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - // SetAuthMode changes how unknown peers are handled. // // registry.SetAuthMode(PeerAuthAllowlist) diff --git a/node/transport.go b/node/transport.go index b0a72fa..4d2ec85 100644 --- a/node/transport.go +++ b/node/transport.go @@ -45,11 +45,9 @@ const ( type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path - WSPath string // Deprecated compatibility alias for WebSocketPath. TLSCertPath string // Optional TLS for wss:// TLSKeyPath string MaxConnections int // Maximum concurrent connections - MaxConns int // Deprecated compatibility alias for MaxConnections. MaxMessageSize int64 // Maximum message size in bytes (0 = 1MB default) PingInterval time.Duration // WebSocket keepalive interval PongTimeout time.Duration // Timeout waiting for pong @@ -62,9 +60,7 @@ func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, WebSocketPath: defaultTransportWebSocketPath, - WSPath: defaultTransportWebSocketPath, MaxConnections: defaultTransportMaximumConnections, - MaxConns: defaultTransportMaximumConnections, MaxMessageSize: DefaultMaxMessageSize, PingInterval: 30 * time.Second, PongTimeout: 10 * time.Second, @@ -80,39 +76,20 @@ func (c TransportConfig) listenAddress() string { return defaultTransportListenAddress } -// webSocketPath returns the effective WebSocket endpoint path, preferring the -// clearer WebSocketPath field and falling back to the compatibility alias. +// webSocketPath returns the effective WebSocket endpoint path. func (c TransportConfig) webSocketPath() string { - switch { - case c.WebSocketPath != "" && c.WebSocketPath != defaultTransportWebSocketPath: + if c.WebSocketPath != "" { return c.WebSocketPath - case c.WSPath != "" && c.WSPath != defaultTransportWebSocketPath: - return c.WSPath - case c.WebSocketPath != "": - return c.WebSocketPath - case c.WSPath != "": - return c.WSPath - default: - return defaultTransportWebSocketPath } + return defaultTransportWebSocketPath } -// maximumConnections returns the effective concurrent connection limit, -// preferring the clearer MaxConnections field and falling back to the -// compatibility alias. +// maximumConnections returns the effective concurrent connection limit. func (c TransportConfig) maximumConnections() int { - switch { - case c.MaxConnections != 0 && c.MaxConnections != defaultTransportMaximumConnections: + if c.MaxConnections > 0 { return c.MaxConnections - case c.MaxConns != 0 && c.MaxConns != defaultTransportMaximumConnections: - return c.MaxConns - case c.MaxConnections != 0: - return c.MaxConnections - case c.MaxConns != 0: - return c.MaxConns - default: - return defaultTransportMaximumConnections } + return defaultTransportMaximumConnections } // MessageHandler processes incoming messages. @@ -1100,16 +1077,16 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeers returns the number of connected peers. +// ConnectedPeerCount returns the number of connected peers. // -// count := transport.ConnectedPeers() -func (t *Transport) ConnectedPeers() int { +// count := transport.ConnectedPeerCount() +func (t *Transport) ConnectedPeerCount() int { t.mutex.RLock() defer t.mutex.RUnlock() return len(t.connections) } -// ConnectedPeerCount is a deprecated compatibility alias for ConnectedPeers. -func (t *Transport) ConnectedPeerCount() int { - return t.ConnectedPeers() +// ConnectedPeers is a deprecated compatibility alias for ConnectedPeerCount. +func (t *Transport) ConnectedPeers() int { + return t.ConnectedPeerCount() } diff --git a/node/worker.go b/node/worker.go index dd034db..8997327 100644 --- a/node/worker.go +++ b/node/worker.go @@ -49,8 +49,6 @@ type Worker struct { profileManager ProfileManager startedAt time.Time DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) - DataDir string // Deprecated compatibility alias for DeploymentDirectory - DataDirectory string // Deprecated compatibility alias for DeploymentDirectory } // NewWorker creates a new Worker instance. @@ -62,8 +60,6 @@ func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { transport: transport, startedAt: time.Now(), DeploymentDirectory: xdg.DataHome, - DataDir: xdg.DataHome, - DataDirectory: xdg.DataHome, } } @@ -424,28 +420,10 @@ func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -// RegisterWithTransport is a deprecated compatibility alias for RegisterOnTransport. -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} - -// deploymentDirectory resolves the active deployment directory, preferring -// DeploymentDirectory unless a caller has only populated the legacy aliases. +// deploymentDirectory resolves the active deployment directory. func (w *Worker) deploymentDirectory() string { - switch { - case w.DeploymentDirectory != "" && w.DeploymentDirectory != xdg.DataHome: + if w.DeploymentDirectory != "" { return w.DeploymentDirectory - case w.DataDir != "" && w.DataDir != xdg.DataHome: - return w.DataDir - case w.DataDirectory != "" && w.DataDirectory != xdg.DataHome: - return w.DataDirectory - case w.DeploymentDirectory != "": - return w.DeploymentDirectory - case w.DataDir != "": - return w.DataDir - case w.DataDirectory != "": - return w.DataDirectory - default: - return xdg.DataHome } + return xdg.DataHome } -- 2.45.3 From 390e60860e24ace17282f2d7ea410ca13e2de330 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 05:08:09 +0000 Subject: [PATCH 28/60] refactor(node): remove legacy AX aliases Co-Authored-By: Virgil --- node/bench_test.go | 4 +-- node/controller.go | 15 ---------- node/controller_test.go | 64 ++++++++++++++++++++-------------------- node/identity.go | 5 ---- node/identity_test.go | 18 +++++------ node/integration_test.go | 50 +++++++++++++++---------------- node/peer.go | 25 ---------------- node/peer_test.go | 56 +++++++++++++++++------------------ node/protocol.go | 5 ---- node/protocol_test.go | 10 +++---- node/transport.go | 10 ------- node/transport_test.go | 50 +++++++++++++++---------------- node/worker_test.go | 42 +++++++++++++------------- 13 files changed, 147 insertions(+), 207 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index f5b0518..b6872b8 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -36,7 +36,7 @@ func BenchmarkDeriveSharedSecret(b *testing.B) { nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) - peerPubKey := nm2.Identity().PublicKey + peerPubKey := nm2.GetIdentity().PublicKey b.ReportAllocs() b.ResetTimer() @@ -151,7 +151,7 @@ func BenchmarkSMSGEncryptDecrypt(b *testing.B) { nm2, _ := NewNodeManagerFromPaths(testJoinPath(dir2, "k"), testJoinPath(dir2, "n")) nm2.GenerateIdentity("node2", RoleDual) - sharedSecret, _ := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) + sharedSecret, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) password := base64.StdEncoding.EncodeToString(sharedSecret) // Prepare a message to encrypt diff --git a/node/controller.go b/node/controller.go index 3a72e4c..2bec4d5 100644 --- a/node/controller.go +++ b/node/controller.go @@ -146,11 +146,6 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } -// RemoteStats is a deprecated compatibility alias for GetRemoteStats. -func (c *Controller) RemoteStats(peerID string) (*StatsPayload, error) { - return c.GetRemoteStats(peerID) -} - // StartRemoteMiner requests a remote peer to start a miner with a given profile. // // err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) @@ -259,11 +254,6 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// RemoteLogs is a deprecated compatibility alias for GetRemoteLogs. -func (c *Controller) RemoteLogs(peerID, minerName string, lines int) ([]string, error) { - return c.GetRemoteLogs(peerID, minerName, lines) -} - // GetAllStats fetches stats from all connected peers. // // statsByPeerID := controller.GetAllStats() @@ -295,11 +285,6 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// AllStats is a deprecated compatibility alias for GetAllStats. -func (c *Controller) AllStats() map[string]*StatsPayload { - return c.GetAllStats() -} - // PingPeer sends a ping to a peer and refreshes that peer's metrics. // // rttMS, err := controller.PingPeer("worker-1") diff --git a/node/controller_test.go b/node/controller_test.go index 03b121e..aa6ede8 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -75,7 +75,7 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { func TestController_RequestResponseCorrelation_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID // Send a ping request via the controller; the server-side worker // replies with MessagePong, setting ReplyTo to the original message ID. @@ -97,8 +97,8 @@ func TestController_RequestTimeout_Bad(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.Identity().ID - clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID + clientID := tp.ClientNode.GetIdentity().ID // Use sendRequest directly with a short deadline (PingPeer uses 5s internally). msg, err := NewMessage(MessagePing, clientID, serverID, PingPayload{ @@ -126,7 +126,7 @@ func TestController_AutoConnect_Good(t *testing.T) { controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) // Add the server peer to the client registry so auto-connect can resolve it. - serverIdentity := tp.ServerNode.Identity() + serverIdentity := tp.ServerNode.GetIdentity() peer := &Peer{ ID: serverIdentity.ID, Name: "server", @@ -159,7 +159,7 @@ func TestController_AllStats_Good(t *testing.T) { for i := range numWorkers { nm, addr, _ := makeWorkerServer(t) - wID := nm.Identity().ID + wID := nm.GetIdentity().ID workerIDs[i] = wID peer := &Peer{ @@ -179,7 +179,7 @@ func TestController_AllStats_Good(t *testing.T) { controller := NewController(controllerNM, controllerReg, controllerTransport) // AllStats fetches stats from all connected peers in parallel. - stats := controller.AllStats() + stats := controller.GetAllStats() assert.Len(t, stats, numWorkers, "should get stats from all connected workers") for _, wID := range workerIDs { @@ -194,10 +194,10 @@ func TestController_AllStats_Good(t *testing.T) { func TestController_PingPeerRTT_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID // Record initial peer metrics. - peerBefore := tp.ClientReg.Peer(serverID) + peerBefore := tp.ClientReg.GetPeer(serverID) require.NotNil(t, peerBefore, "server peer should exist in the client registry") initialPingMS := peerBefore.PingMS @@ -208,7 +208,7 @@ func TestController_PingPeerRTT_Good(t *testing.T) { assert.Less(t, rtt, 1000.0, "RTT on loopback should be well under 1000ms") // Verify the peer registry was updated with the measured latency. - peerAfter := tp.ClientReg.Peer(serverID) + peerAfter := tp.ClientReg.GetPeer(serverID) require.NotNil(t, peerAfter, "server peer should still exist after ping") assert.NotEqual(t, initialPingMS, peerAfter.PingMS, "PingMS should be updated after a successful ping") @@ -228,7 +228,7 @@ func TestController_ConcurrentRequests_Ugly(t *testing.T) { for i := range numPeers { nm, addr, _ := makeWorkerServer(t) - pID := nm.Identity().ID + pID := nm.GetIdentity().ID peerIDs[i] = pID peer := &Peer{ @@ -279,8 +279,8 @@ func TestController_DeadPeerCleanup_Good(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.Identity().ID - clientID := tp.ClientNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID + clientID := tp.ClientNode.GetIdentity().ID // Fire off a request that will time out. msg, err := NewMessage(MessagePing, clientID, serverID, PingPayload{ @@ -308,7 +308,7 @@ func TestController_DeadPeerCleanup_Good(t *testing.T) { func TestController_MultipleSequentialPings_Good(t *testing.T) { // Ensures sequential requests to the same peer are correctly correlated. controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID for i := range 5 { rtt, err := controller.PingPeer(serverID) @@ -321,7 +321,7 @@ func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { // Multiple goroutines sending requests to the SAME peer simultaneously. // Tests concurrent pending-map insertions/deletions under contention. controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID const goroutines = 10 var wg sync.WaitGroup @@ -343,9 +343,9 @@ func TestController_ConcurrentRequestsSamePeer_Ugly(t *testing.T) { func TestController_RemoteStats_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - stats, err := controller.RemoteStats(serverID) + stats, err := controller.GetRemoteStats(serverID) require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) @@ -366,7 +366,7 @@ func TestController_ConnectToPeerUnknown_Bad(t *testing.T) { func TestController_DisconnectFromPeer_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID assert.Equal(t, 1, tp.Client.ConnectedPeerCount(), "should have 1 connection") @@ -387,7 +387,7 @@ func TestController_SendRequestPeerNotFound_Bad(t *testing.T) { tp := setupTestTransportPair(t) controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) - clientID := tp.ClientNode.Identity().ID + clientID := tp.ClientNode.GetIdentity().ID msg, err := NewMessage(MessagePing, clientID, "ghost-peer", PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -530,7 +530,7 @@ func (m *mockMinerFull) ConsoleHistory(lines int) []string { return m.GetConsole func TestController_StartRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID configOverride := RawMessage(`{"pool":"pool.example.com:3333"}`) err := controller.StartRemoteMiner(serverID, "xmrig", "profile-1", configOverride) @@ -539,7 +539,7 @@ func TestController_StartRemoteMiner_Good(t *testing.T) { func TestController_StartRemoteMiner_WithConfig_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID configOverride := RawMessage(`{"pool":"custom-pool:3333","threads":4}`) err := controller.StartRemoteMiner(serverID, "xmrig", "", configOverride) @@ -548,7 +548,7 @@ func TestController_StartRemoteMiner_WithConfig_Good(t *testing.T) { func TestController_StartRemoteMiner_EmptyType_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID err := controller.StartRemoteMiner(serverID, "", "profile-1", nil) require.Error(t, err, "StartRemoteMiner with empty miner type should fail") @@ -572,7 +572,7 @@ func TestController_StartRemoteMiner_NoIdentity_Bad(t *testing.T) { func TestController_StopRemoteMiner_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID err := controller.StopRemoteMiner(serverID, "running-miner") require.NoError(t, err, "StopRemoteMiner should succeed for existing miner") @@ -580,7 +580,7 @@ func TestController_StopRemoteMiner_Good(t *testing.T) { func TestController_StopRemoteMiner_NotFound_Bad(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID err := controller.StopRemoteMiner(serverID, "non-existent-miner") require.Error(t, err, "StopRemoteMiner should fail for non-existent miner") @@ -601,9 +601,9 @@ func TestController_StopRemoteMiner_NoIdentity_Bad(t *testing.T) { func TestController_RemoteLogs_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - lines, err := controller.RemoteLogs(serverID, "running-miner", 10) + lines, err := controller.GetRemoteLogs(serverID, "running-miner", 10) require.NoError(t, err, "RemoteLogs should succeed") require.NotNil(t, lines) assert.Len(t, lines, 3, "should return all 3 console history lines") @@ -612,9 +612,9 @@ func TestController_RemoteLogs_Good(t *testing.T) { func TestController_RemoteLogs_LimitedLines_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - lines, err := controller.RemoteLogs(serverID, "running-miner", 1) + lines, err := controller.GetRemoteLogs(serverID, "running-miner", 1) require.NoError(t, err, "RemoteLogs with limited lines should succeed") assert.Len(t, lines, 1, "should return only 1 line") } @@ -627,16 +627,16 @@ func TestController_RemoteLogs_NoIdentity_Bad(t *testing.T) { controller := NewController(nmNoID, tp.ClientReg, tp.Client) - _, err = controller.RemoteLogs("some-peer", "any-miner", 10) + _, err = controller.GetRemoteLogs("some-peer", "any-miner", 10) require.Error(t, err) assert.Contains(t, err.Error(), "identity not initialized") } func TestController_RemoteStats_WithMiners_Good(t *testing.T) { controller, _, tp := setupControllerPairWithMiner(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - stats, err := controller.RemoteStats(serverID) + stats, err := controller.GetRemoteStats(serverID) require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) assert.NotEmpty(t, stats.NodeID) @@ -654,7 +654,7 @@ func TestController_RemoteStats_NoIdentity_Bad(t *testing.T) { controller := NewController(nmNoID, tp.ClientReg, tp.Client) - _, err = controller.RemoteStats("some-peer") + _, err = controller.GetRemoteStats("some-peer") require.Error(t, err) assert.Contains(t, err.Error(), "identity not initialized") } @@ -668,7 +668,7 @@ func TestController_ConnectToPeer_Success_Good(t *testing.T) { controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client) // Add the server peer to the client registry. - serverIdentity := tp.ServerNode.Identity() + serverIdentity := tp.ServerNode.GetIdentity() peer := &Peer{ ID: serverIdentity.ID, Name: "server", diff --git a/node/identity.go b/node/identity.go index 4acdd74..fc2aa2d 100644 --- a/node/identity.go +++ b/node/identity.go @@ -147,11 +147,6 @@ func (n *NodeManager) GetIdentity() *NodeIdentity { return &identity } -// Identity is a deprecated compatibility alias for GetIdentity. -func (n *NodeManager) Identity() *NodeIdentity { - return n.GetIdentity() -} - // GenerateIdentity writes a new node identity for the given name and role. // // err := nodeManager.GenerateIdentity("worker-1", RoleWorker) diff --git a/node/identity_test.go b/node/identity_test.go index d4be4cb..f72d8ef 100644 --- a/node/identity_test.go +++ b/node/identity_test.go @@ -35,7 +35,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Error("node manager should have identity after generation") } - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("identity should not be nil") } @@ -72,8 +72,8 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Fatalf("failed to generate identity: %v", err) } - originalID := nm1.Identity().ID - originalPubKey := nm1.Identity().PublicKey + originalID := nm1.GetIdentity().ID + originalPubKey := nm1.GetIdentity().PublicKey // Create a new manager - should load existing identity nm2, err := NewNodeManagerFromPaths(keyPath, configPath) @@ -85,7 +85,7 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { t.Error("second node manager should have loaded existing identity") } - identity := nm2.Identity() + identity := nm2.GetIdentity() if identity.ID != originalID { t.Errorf("expected ID '%s', got '%s'", originalID, identity.ID) } @@ -121,12 +121,12 @@ func TestIdentity_NodeIdentity_Good(t *testing.T) { } // Derive shared secrets - should be identical - secret1, err := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) + secret1, err := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) if err != nil { t.Fatalf("failed to derive shared secret from node 1: %v", err) } - secret2, err := nm2.DeriveSharedSecret(nm1.Identity().PublicKey) + secret2, err := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey) if err != nil { t.Fatalf("failed to derive shared secret from node 2: %v", err) } @@ -315,8 +315,8 @@ func TestIdentity_ChallengeResponse_Good(t *testing.T) { } // Both derive the same shared secret - secret1, _ := nm1.DeriveSharedSecret(nm2.Identity().PublicKey) - secret2, _ := nm2.DeriveSharedSecret(nm1.Identity().PublicKey) + secret1, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey) + secret2, _ := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey) // Responder signs challenge with their derived secret response := SignChallenge(challenge, secret2) @@ -341,7 +341,7 @@ func TestIdentity_NodeManager_DeriveSharedSecret_NoIdentity_Bad(t *testing.T) { func TestIdentity_NodeManager_Identity_NilWhenNoIdentity_Bad(t *testing.T) { nm := newTestNodeManagerWithoutIdentity(t) - identity := nm.Identity() + identity := nm.GetIdentity() if identity != nil { t.Error("expected nil identity before generation") } diff --git a/node/integration_test.go b/node/integration_test.go index 9eadf12..f523457 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -34,8 +34,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { controllerNM := newTestNodeManager(t, "integration-controller", RoleController) workerNM := newTestNodeManager(t, "integration-worker", RoleWorker) - controllerIdentity := controllerNM.Identity() - workerIdentity := workerNM.Identity() + controllerIdentity := controllerNM.GetIdentity() + workerIdentity := workerNM.GetIdentity() require.NotNil(t, controllerIdentity, "controller identity should be initialised") require.NotNil(t, workerIdentity, "worker identity should be initialised") assert.NotEmpty(t, controllerIdentity.ID, "controller ID should be non-empty") @@ -122,8 +122,8 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { "worker should have 1 connected peer") // Verify the peer's real identity is stored. - serverPeerID := workerNM.Identity().ID - conn := controllerTransport.Connection(serverPeerID) + serverPeerID := workerNM.GetIdentity().ID + conn := controllerTransport.GetConnection(serverPeerID) require.NotNil(t, conn, "controller should hold a connection keyed by server's real ID") assert.Equal(t, "integration-worker", conn.Peer.Name) @@ -136,14 +136,14 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { assert.Less(t, rtt, 1000.0, "RTT on loopback should be well under 1s") // Verify registry metrics were updated. - peerAfterPing := controllerReg.Peer(serverPeerID) + peerAfterPing := controllerReg.GetPeer(serverPeerID) require.NotNil(t, peerAfterPing) assert.Greater(t, peerAfterPing.PingMS, 0.0, "PingMS should be updated") // ---------------------------------------------------------------- // Step 5: Encrypted message exchange — RemoteStats // ---------------------------------------------------------------- - stats, err := controller.RemoteStats(serverPeerID) + stats, err := controller.GetRemoteStats(serverPeerID) require.NoError(t, err, "RemoteStats should succeed") require.NotNil(t, stats) assert.Equal(t, workerIdentity.ID, stats.NodeID) @@ -242,8 +242,8 @@ func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { nodeA := newTestNodeManager(t, "secret-node-a", RoleDual) nodeB := newTestNodeManager(t, "secret-node-b", RoleDual) - pubKeyA := nodeA.Identity().PublicKey - pubKeyB := nodeB.Identity().PublicKey + pubKeyA := nodeA.GetIdentity().PublicKey + pubKeyB := nodeB.GetIdentity().PublicKey secretFromA, err := nodeA.DeriveSharedSecret(pubKeyB) require.NoError(t, err) @@ -260,7 +260,7 @@ func TestIntegration_SharedSecretAgreement_Good(t *testing.T) { // can send and receive encrypted messages after the handshake. func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { controller, _, tp := setupControllerPair(t) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID // Controller -> Worker: Ping rtt, err := controller.PingPeer(serverID) @@ -268,7 +268,7 @@ func TestIntegration_TwoNodeBidirectionalMessages_Good(t *testing.T) { assert.Greater(t, rtt, 0.0) // Controller -> Worker: GetStats - stats, err := controller.RemoteStats(serverID) + stats, err := controller.GetRemoteStats(serverID) require.NoError(t, err) require.NotNil(t, stats) assert.NotEmpty(t, stats.NodeID) @@ -294,7 +294,7 @@ func TestIntegration_MultiPeerTopology_Good(t *testing.T) { for i := range numWorkers { nm, addr, _ := makeWorkerServer(t) - wID := nm.Identity().ID + wID := nm.GetIdentity().ID workerIDs[i] = wID peer := &Peer{ @@ -335,7 +335,7 @@ func TestIntegration_MultiPeerTopology_Good(t *testing.T) { } // Fetch stats from all workers in parallel. - allStats := controller.AllStats() + allStats := controller.GetAllStats() assert.Len(t, allStats, numWorkers, "should get stats from all workers") } @@ -350,7 +350,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { require.NoError(t, err) require.NoError(t, nm1.GenerateIdentity("persistent-node", RoleDual)) - original := nm1.Identity() + original := nm1.GetIdentity() require.NotNil(t, original) // Reload from disk. @@ -358,7 +358,7 @@ func TestIntegration_IdentityPersistenceAndReload_Good(t *testing.T) { require.NoError(t, err) require.True(t, nm2.HasIdentity(), "identity should be loaded from disk") - reloaded := nm2.Identity() + reloaded := nm2.GetIdentity() require.NotNil(t, reloaded) assert.Equal(t, original.ID, reloaded.ID, "ID should persist") @@ -390,7 +390,7 @@ func stmfGenerateKeyPair(dir string) (string, error) { if err := nm.GenerateIdentity("temp-peer", RoleWorker); err != nil { return "", err } - return nm.Identity().PublicKey, nil + return nm.GetIdentity().PublicKey, nil } // TestIntegration_UEPSFullRoundTrip exercises a complete UEPS packet @@ -399,7 +399,7 @@ func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { nodeA := newTestNodeManager(t, "ueps-node-a", RoleController) nodeB := newTestNodeManager(t, "ueps-node-b", RoleWorker) - bPubKey := nodeB.Identity().PublicKey + bPubKey := nodeB.GetIdentity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) require.NoError(t, err, "shared secret derivation should succeed") require.Len(t, sharedSecret, 32, "shared secret should be 32 bytes (SHA-256)") @@ -414,7 +414,7 @@ func TestIntegration_UEPSFullRoundTrip_Ugly(t *testing.T) { require.NotEmpty(t, wireData) // Node B derives the same shared secret from A's public key. - aPubKey := nodeA.Identity().PublicKey + aPubKey := nodeA.GetIdentity().PublicKey sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey) require.NoError(t, err) assert.Equal(t, sharedSecret, sharedSecretB, @@ -450,7 +450,7 @@ func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { nodeA := newTestNodeManager(t, "integrity-a", RoleController) nodeB := newTestNodeManager(t, "integrity-b", RoleWorker) - bPubKey := nodeB.Identity().PublicKey + bPubKey := nodeB.GetIdentity().PublicKey sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey) require.NoError(t, err) @@ -463,7 +463,7 @@ func TestIntegration_UEPSIntegrityFailure_Bad(t *testing.T) { copy(tampered, wireData) tampered[len(tampered)-1] ^= 0xFF - aPubKey := nodeA.Identity().PublicKey + aPubKey := nodeA.GetIdentity().PublicKey sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey) require.NoError(t, err) @@ -500,7 +500,7 @@ func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { t.Cleanup(func() { controllerTransport.Stop() }) peer := &Peer{ - ID: workerNM.Identity().ID, + ID: workerNM.GetIdentity().ID, Name: "worker", Address: u.Host, Role: RoleWorker, @@ -522,7 +522,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { controllerNM := newTestNodeManager(t, "allowed-controller", RoleController) controllerReg := newTestPeerRegistry(t) - workerReg.AllowPublicKey(controllerNM.Identity().PublicKey) + workerReg.AllowPublicKey(controllerNM.GetIdentity().PublicKey) workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig()) worker := NewWorker(workerNM, workerTransport) @@ -542,7 +542,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { t.Cleanup(func() { controllerTransport.Stop() }) peer := &Peer{ - ID: workerNM.Identity().ID, + ID: workerNM.GetIdentity().ID, Name: "worker", Address: u.Host, Role: RoleWorker, @@ -611,7 +611,7 @@ func TestIntegration_MessageSerialiseDeserialise_Good(t *testing.T) { tp := setupTestTransportPair(t) pc := tp.connectClient(t) - original, err := NewMessage(MessageStats, tp.ClientNode.Identity().ID, tp.ServerNode.Identity().ID, StatsPayload{ + original, err := NewMessage(MessageStats, tp.ClientNode.GetIdentity().ID, tp.ServerNode.GetIdentity().ID, StatsPayload{ NodeID: "test-node", NodeName: "test-name", Miners: []MinerStatsItem{ @@ -664,9 +664,9 @@ func TestIntegration_RemoteStats_EndToEnd_Good(t *testing.T) { tp.connectClient(t) time.Sleep(100 * time.Millisecond) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - stats, err := controller.RemoteStats(serverID) + stats, err := controller.GetRemoteStats(serverID) require.NoError(t, err, "RemoteStats should succeed end-to-end") require.NotNil(t, stats) assert.Equal(t, serverID, stats.NodeID) diff --git a/node/peer.go b/node/peer.go index c17c55b..8d7458a 100644 --- a/node/peer.go +++ b/node/peer.go @@ -179,11 +179,6 @@ func (r *PeerRegistry) GetAuthMode() PeerAuthMode { return r.authMode } -// AuthMode is a deprecated compatibility alias for GetAuthMode. -func (r *PeerRegistry) AuthMode() PeerAuthMode { - return r.GetAuthMode() -} - // AllowPublicKey adds a public key to the allowlist. func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() @@ -345,11 +340,6 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return &peerCopy } -// Peer is a deprecated compatibility alias for GetPeer. -func (r *PeerRegistry) Peer(id string) *Peer { - return r.GetPeer(id) -} - // ListPeers returns all registered peers. func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) @@ -431,11 +421,6 @@ func (r *PeerRegistry) SetConnected(id string, connected bool) { } } -// MarkConnected is a deprecated compatibility alias for SetConnected. -func (r *PeerRegistry) MarkConnected(id string, connected bool) { - r.SetConnected(id, connected) -} - // Score adjustment constants const ( ScoreSuccessIncrement = 1.0 // Increment for successful interaction @@ -526,11 +511,6 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return peers } -// PeersSortedByScore is a deprecated compatibility alias for GetPeersByScore. -func (r *PeerRegistry) PeersSortedByScore() []*Peer { - return r.GetPeersByScore() -} - // PeersByScore returns an iterator over peers sorted by score (highest first). func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { @@ -603,11 +583,6 @@ func (r *PeerRegistry) GetConnectedPeers() []*Peer { return slices.Collect(r.ConnectedPeers()) } -// ConnectedPeerList is a deprecated compatibility alias for GetConnectedPeers. -func (r *PeerRegistry) ConnectedPeerList() []*Peer { - return r.GetConnectedPeers() -} - // ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { diff --git a/node/peer_test.go b/node/peer_test.go index e6799be..6a79c79 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -86,7 +86,7 @@ func TestPeer_Registry_Peer_Good(t *testing.T) { pr.AddPeer(peer) - retrieved := pr.Peer("get-test-peer") + retrieved := pr.GetPeer("get-test-peer") if retrieved == nil { t.Fatal("failed to retrieve peer") } @@ -96,7 +96,7 @@ func TestPeer_Registry_Peer_Good(t *testing.T) { } // Non-existent peer - nonExistent := pr.Peer("non-existent") + nonExistent := pr.GetPeer("non-existent") if nonExistent != nil { t.Error("expected nil for non-existent peer") } @@ -173,7 +173,7 @@ func TestPeer_Registry_UpdateMetrics_Good(t *testing.T) { t.Fatalf("failed to update metrics: %v", err) } - updated := pr.Peer("metrics-test") + updated := pr.GetPeer("metrics-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -205,7 +205,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated := pr.Peer("score-test") + updated := pr.GetPeer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -219,7 +219,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated = pr.Peer("score-test") + updated = pr.GetPeer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -233,7 +233,7 @@ func TestPeer_Registry_UpdateScore_Good(t *testing.T) { t.Fatalf("failed to update score: %v", err) } - updated = pr.Peer("score-test") + updated = pr.GetPeer("score-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -254,9 +254,9 @@ func TestPeer_Registry_MarkConnected_Good(t *testing.T) { pr.AddPeer(peer) - pr.MarkConnected("connect-test", true) + pr.SetConnected("connect-test", true) - updated := pr.Peer("connect-test") + updated := pr.GetPeer("connect-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -267,8 +267,8 @@ func TestPeer_Registry_MarkConnected_Good(t *testing.T) { t.Error("LastSeen should be set when connected") } - pr.MarkConnected("connect-test", false) - updated = pr.Peer("connect-test") + pr.SetConnected("connect-test", false) + updated = pr.GetPeer("connect-test") if updated == nil { t.Fatal("expected peer to exist") } @@ -291,10 +291,10 @@ func TestPeer_Registry_ConnectedPeerList_Good(t *testing.T) { pr.AddPeer(p) } - pr.MarkConnected("conn-1", true) - pr.MarkConnected("conn-3", true) + pr.SetConnected("conn-1", true) + pr.SetConnected("conn-3", true) - connected := pr.ConnectedPeerList() + connected := pr.GetConnectedPeers() if len(connected) != 2 { t.Errorf("expected 2 connected peers, got %d", len(connected)) } @@ -382,7 +382,7 @@ func TestPeer_Registry_Persistence_Good(t *testing.T) { t.Errorf("expected 1 peer after reload, got %d", pr2.Count()) } - loaded := pr2.Peer("persist-test") + loaded := pr2.GetPeer("persist-test") if loaded == nil { t.Fatal("peer should exist after reload") } @@ -399,20 +399,20 @@ func TestPeer_Registry_AuthMode_Good(t *testing.T) { defer cleanup() // Default should be Open - if pr.AuthMode() != PeerAuthOpen { - t.Errorf("expected default auth mode to be Open, got %d", pr.AuthMode()) + if pr.GetAuthMode() != PeerAuthOpen { + t.Errorf("expected default auth mode to be Open, got %d", pr.GetAuthMode()) } // Set to Allowlist pr.SetAuthMode(PeerAuthAllowlist) - if pr.AuthMode() != PeerAuthAllowlist { - t.Errorf("expected auth mode to be Allowlist after setting, got %d", pr.AuthMode()) + if pr.GetAuthMode() != PeerAuthAllowlist { + t.Errorf("expected auth mode to be Allowlist after setting, got %d", pr.GetAuthMode()) } // Set back to Open pr.SetAuthMode(PeerAuthOpen) - if pr.AuthMode() != PeerAuthOpen { - t.Errorf("expected auth mode to be Open after resetting, got %d", pr.AuthMode()) + if pr.GetAuthMode() != PeerAuthOpen { + t.Errorf("expected auth mode to be Open after resetting, got %d", pr.GetAuthMode()) } } @@ -563,7 +563,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 5 { pr.RecordSuccess("score-record-test") } - updated := pr.Peer("score-record-test") + updated := pr.GetPeer("score-record-test") if updated.Score <= 50 { t.Errorf("score should increase after successes, got %f", updated.Score) } @@ -573,7 +573,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 3 { pr.RecordFailure("score-record-test") } - updated = pr.Peer("score-record-test") + updated = pr.GetPeer("score-record-test") if updated.Score >= initialScore { t.Errorf("score should decrease after failures, got %f (was %f)", updated.Score, initialScore) } @@ -581,7 +581,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { // Record timeouts - score should decrease initialScore = updated.Score pr.RecordTimeout("score-record-test") - updated = pr.Peer("score-record-test") + updated = pr.GetPeer("score-record-test") if updated.Score >= initialScore { t.Errorf("score should decrease after timeout, got %f (was %f)", updated.Score, initialScore) } @@ -590,7 +590,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 100 { pr.RecordSuccess("score-record-test") } - updated = pr.Peer("score-record-test") + updated = pr.GetPeer("score-record-test") if updated.Score > ScoreMaximum { t.Errorf("score should be clamped to max %f, got %f", ScoreMaximum, updated.Score) } @@ -598,7 +598,7 @@ func TestPeer_Registry_ScoreRecording_Good(t *testing.T) { for range 100 { pr.RecordFailure("score-record-test") } - updated = pr.Peer("score-record-test") + updated = pr.GetPeer("score-record-test") if updated.Score < ScoreMinimum { t.Errorf("score should be clamped to min %f, got %f", ScoreMinimum, updated.Score) } @@ -619,7 +619,7 @@ func TestPeer_Registry_PeersSortedByScore_Good(t *testing.T) { pr.AddPeer(p) } - sorted := pr.PeersSortedByScore() + sorted := pr.GetPeersByScore() if len(sorted) != 3 { t.Fatalf("expected 3 peers, got %d", len(sorted)) } @@ -726,7 +726,7 @@ func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { t.Fatalf("failed to update peer: %v", err) } - updated := pr.Peer("update-test") + updated := pr.GetPeer("update-test") if updated == nil { t.Fatal("expected peer to exist after update") } @@ -805,7 +805,7 @@ func TestPeer_Registry_MarkConnected_NonExistent_Bad(t *testing.T) { defer cleanup() // Should not panic for non-existent peer - pr.MarkConnected("ghost-peer", true) + pr.SetConnected("ghost-peer", true) } func TestPeer_Registry_Close_NoDirtyData_Ugly(t *testing.T) { diff --git a/node/protocol.go b/node/protocol.go index 1476294..bc11502 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -97,8 +97,3 @@ func GetProtocolErrorCode(err error) int { } return 0 } - -// ProtocolErrorCode is a deprecated compatibility alias for GetProtocolErrorCode. -func ProtocolErrorCode(err error) int { - return GetProtocolErrorCode(err) -} diff --git a/node/protocol_test.go b/node/protocol_test.go index d609d9f..5598c55 100644 --- a/node/protocol_test.go +++ b/node/protocol_test.go @@ -27,8 +27,8 @@ func TestProtocol_ResponseHandler_ValidateResponse_Good(t *testing.T) { t.Errorf("Expected ProtocolError, got %T", err) } - if ProtocolErrorCode(err) != ErrorCodeOperationFailed { - t.Errorf("Expected code %d, got %d", ErrorCodeOperationFailed, ProtocolErrorCode(err)) + if GetProtocolErrorCode(err) != ErrorCodeOperationFailed { + t.Errorf("Expected code %d, got %d", ErrorCodeOperationFailed, GetProtocolErrorCode(err)) } }) @@ -131,8 +131,8 @@ func TestProtocol_Error_Bad(t *testing.T) { t.Error("IsProtocolError should return true") } - if ProtocolErrorCode(err) != 1001 { - t.Errorf("Expected code 1001, got %d", ProtocolErrorCode(err)) + if GetProtocolErrorCode(err) != 1001 { + t.Errorf("Expected code 1001, got %d", GetProtocolErrorCode(err)) } } @@ -156,7 +156,7 @@ func TestProtocol_ConvenienceFunctions_Good(t *testing.T) { func TestProtocol_ProtocolErrorCode_NonProtocolError_Bad(t *testing.T) { err := core.NewError("regular error") - if ProtocolErrorCode(err) != 0 { + if GetProtocolErrorCode(err) != 0 { t.Error("Expected 0 for non-ProtocolError") } } diff --git a/node/transport.go b/node/transport.go index 4d2ec85..d92dc29 100644 --- a/node/transport.go +++ b/node/transport.go @@ -533,11 +533,6 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { return t.connections[peerID] } -// Connection is a deprecated compatibility alias for GetConnection. -func (t *Transport) Connection(peerID string) *PeerConnection { - return t.GetConnection(peerID) -} - // handleWSUpgrade handles incoming WebSocket connections. func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -1085,8 +1080,3 @@ func (t *Transport) ConnectedPeerCount() int { defer t.mutex.RUnlock() return len(t.connections) } - -// ConnectedPeers is a deprecated compatibility alias for ConnectedPeerCount. -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} diff --git a/node/transport_test.go b/node/transport_test.go index 6416995..424da51 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -102,7 +102,7 @@ func (tp *testTransportPair) connectClient(t *testing.T) *PeerConnection { t.Helper() peer := &Peer{ - ID: tp.ServerNode.Identity().ID, + ID: tp.ServerNode.GetIdentity().ID, Name: "server", Address: tp.ServerAddr, Role: RoleWorker, @@ -227,8 +227,8 @@ func TestTransport_FullHandshake_Good(t *testing.T) { } // Verify peer identity was exchanged correctly - serverID := tp.ServerNode.Identity().ID - serverConn := tp.Client.Connection(serverID) + serverID := tp.ServerNode.GetIdentity().ID + serverConn := tp.Client.GetConnection(serverID) if serverConn == nil { t.Fatal("client should have connection to server by server ID") } @@ -267,7 +267,7 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { serverAddr := u.Host peer := &Peer{ - ID: serverNM.Identity().ID, + ID: serverNM.GetIdentity().ID, Name: "server", Address: serverAddr, Role: RoleWorker, @@ -286,14 +286,14 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { if !strings.HasPrefix(ua, agentUserAgentPrefix) { t.Fatalf("user-agent prefix: got %q, want prefix %q", ua, agentUserAgentPrefix) } - if !strings.Contains(ua, "id="+clientNM.Identity().ID) { + if !strings.Contains(ua, "id="+clientNM.GetIdentity().ID) { t.Fatalf("user-agent should include client identity, got %q", ua) } if pc.UserAgent != ua { t.Fatalf("client connection user-agent: got %q, want %q", pc.UserAgent, ua) } - serverConn := serverTransport.Connection(clientNM.Identity().ID) + serverConn := serverTransport.GetConnection(clientNM.GetIdentity().ID) if serverConn == nil { t.Fatal("server should retain the accepted connection") } @@ -313,7 +313,7 @@ func TestTransport_HandshakeRejectWrongVersion_Bad(t *testing.T) { } defer conn.Close() - clientIdentity := tp.ClientNode.Identity() + clientIdentity := tp.ClientNode.GetIdentity() payload := HandshakePayload{ Identity: *clientIdentity, Version: "99.99", // Unsupported @@ -351,7 +351,7 @@ func TestTransport_HandshakeRejectAllowlist_Bad(t *testing.T) { tp.ServerReg.SetAuthMode(PeerAuthAllowlist) peer := &Peer{ - ID: tp.ServerNode.Identity().ID, + ID: tp.ServerNode.GetIdentity().ID, Name: "server", Address: tp.ServerAddr, Role: RoleWorker, @@ -378,8 +378,8 @@ func TestTransport_EncryptedMessageRoundTrip_Ugly(t *testing.T) { pc := tp.connectClient(t) // Send an encrypted message from client to server - clientID := tp.ClientNode.Identity().ID - serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.GetIdentity().ID sentMsg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -420,8 +420,8 @@ func TestTransport_MessageDedup_Good(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.Identity().ID - serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.GetIdentity().ID msg, _ := NewMessage(MessagePing, clientID, serverID, PingPayload{SentAt: time.Now().UnixMilli()}) // Send the same message twice @@ -450,8 +450,8 @@ func TestTransport_RateLimiting_Good(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.Identity().ID - serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.GetIdentity().ID // Send 150 messages rapidly (rate limiter burst = 100) for range 150 { @@ -498,7 +498,7 @@ func TestTransport_MaxConnectionsEnforcement_Good(t *testing.T) { client1Transport := NewTransport(client1NM, client1Reg, DefaultTransportConfig()) t.Cleanup(func() { client1Transport.Stop() }) - peer1 := &Peer{ID: serverNM.Identity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} + peer1 := &Peer{ID: serverNM.GetIdentity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} client1Reg.AddPeer(peer1) _, err := client1Transport.Connect(peer1) @@ -515,7 +515,7 @@ func TestTransport_MaxConnectionsEnforcement_Good(t *testing.T) { client2Transport := NewTransport(client2NM, client2Reg, DefaultTransportConfig()) t.Cleanup(func() { client2Transport.Stop() }) - peer2 := &Peer{ID: serverNM.Identity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} + peer2 := &Peer{ID: serverNM.GetIdentity().ID, Name: "server", Address: serverAddr, Role: RoleWorker} client2Reg.AddPeer(peer2) _, err = client2Transport.Connect(peer2) @@ -545,9 +545,9 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { // Close the underlying WebSocket on the client side to simulate network failure. // The server's readLoop will detect the broken connection and clean up. - clientID := tp.ClientNode.Identity().ID - serverPeerID := tp.ServerNode.Identity().ID - clientConn := tp.Client.Connection(serverPeerID) + clientID := tp.ClientNode.GetIdentity().ID + serverPeerID := tp.ServerNode.GetIdentity().ID + clientConn := tp.Client.GetConnection(serverPeerID) if clientConn == nil { t.Fatal("client should have connection to server") } @@ -562,7 +562,7 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { default: if tp.Server.ConnectedPeerCount() == 0 { // Verify registry updated - peer := tp.ServerReg.Peer(clientID) + peer := tp.ServerReg.GetPeer(clientID) if peer != nil && peer.Connected { t.Error("registry should show peer as disconnected") } @@ -618,8 +618,8 @@ func TestTransport_ConcurrentSends_Ugly(t *testing.T) { pc := tp.connectClient(t) - clientID := tp.ClientNode.Identity().ID - serverID := tp.ServerNode.Identity().ID + clientID := tp.ClientNode.GetIdentity().ID + serverID := tp.ServerNode.GetIdentity().ID // Spawn 10 goroutines each sending 5 messages concurrently const goroutines = 10 @@ -667,7 +667,7 @@ func TestTransport_Broadcast_Good(t *testing.T) { counter.Add(1) }) - wID := nm.Identity().ID + wID := nm.GetIdentity().ID peer := &Peer{ ID: wID, Name: "worker", @@ -685,7 +685,7 @@ func TestTransport_Broadcast_Good(t *testing.T) { time.Sleep(100 * time.Millisecond) // Broadcast a message from the controller - controllerID := controllerNM.Identity().ID + controllerID := controllerNM.GetIdentity().ID msg, _ := NewMessage(MessagePing, controllerID, "", PingPayload{ SentAt: time.Now().UnixMilli(), }) @@ -721,7 +721,7 @@ func TestTransport_BroadcastExcludesSender_Good(t *testing.T) { // The server has a connection to the client, but msg.From matches the client's // connection peer ID check, not the server's own ID. Let's verify sender exclusion // by broadcasting from the server with its own ID. - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID msg, _ := NewMessage(MessagePing, serverID, "", PingPayload{SentAt: time.Now().UnixMilli()}) // This broadcasts from server to all connected peers (the client). diff --git a/node/worker_test.go b/node/worker_test.go index 24b39fe..5915b33 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -131,7 +131,7 @@ func TestWorker_HandlePing_Good(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a ping message - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -192,7 +192,7 @@ func TestWorker_HandleStats_Good(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a stats request message. - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -252,7 +252,7 @@ func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a start_miner message - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -292,7 +292,7 @@ func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a stop_miner message - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -332,7 +332,7 @@ func TestWorker_HandleLogs_NoManager_Bad(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a logs request message. - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -372,7 +372,7 @@ func TestWorker_HandleDeploy_Profile_Good(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a deploy message for profile - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -416,7 +416,7 @@ func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) { worker.DeploymentDirectory = t.TempDir() // Create a deploy message with unknown type - identity := nm.Identity() + identity := nm.GetIdentity() if identity == nil { t.Fatal("expected identity to be generated") } @@ -629,7 +629,7 @@ func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) { mmFull := &mockMinerManagerWithStart{} worker.SetMinerManager(mmFull) - identity := nm.Identity() + identity := nm.GetIdentity() t.Run("ConfigOverride", func(t *testing.T) { payload := StartMinerPayload{ @@ -788,7 +788,7 @@ func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() t.Run("Success", func(t *testing.T) { worker.SetMinerManager(&mockMinerManager{}) @@ -852,7 +852,7 @@ func TestWorker_HandleLogs_WithManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() t.Run("Success", func(t *testing.T) { mm := &mockMinerManager{ @@ -959,7 +959,7 @@ func TestWorker_HandleStats_WithMinerManager_Good(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() // Set miner manager with miners that have real stats mm := &mockMinerManager{ @@ -1021,7 +1021,7 @@ func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) { worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() msg, _ := NewMessage("unknown_type", "sender-id", identity.ID, nil) // HandleMessage with unknown type should return silently (no panic) @@ -1051,7 +1051,7 @@ func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) { pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) - identity := nm.Identity() + identity := nm.GetIdentity() // Create an unencrypted profile bundle for deploy profileJSON := []byte(`{"id": "deploy-test", "name": "Test Profile"}`) @@ -1104,7 +1104,7 @@ func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) { worker.DeploymentDirectory = t.TempDir() worker.SetProfileManager(&mockProfileManagerFailing{}) - identity := nm.Identity() + identity := nm.GetIdentity() profileJSON := []byte(`{"id": "fail-test"}`) bundle, _ := CreateProfileBundleUnencrypted(profileJSON, "fail-test") @@ -1151,7 +1151,7 @@ func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) { pm := &mockProfileManagerFull{profiles: make(map[string]any)} worker.SetProfileManager(pm) - identity := nm.Identity() + identity := nm.GetIdentity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1213,7 +1213,7 @@ func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) { worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1272,7 +1272,7 @@ func TestWorker_HandleDeploy_MinerBundle_ProfileManager_Good(t *testing.T) { // Set a failing profile manager to exercise the warn-and-continue path worker.SetProfileManager(&mockProfileManagerFailing{}) - identity := nm.Identity() + identity := nm.GetIdentity() tmpDir := t.TempDir() minerPath := testJoinPath(tmpDir, "test-miner") @@ -1321,7 +1321,7 @@ func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) { transport := NewTransport(nm, pr, DefaultTransportConfig()) worker := NewWorker(nm, transport) worker.DeploymentDirectory = t.TempDir() - identity := nm.Identity() + identity := nm.GetIdentity() // Create a message with invalid payload msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, "invalid-payload-not-struct") @@ -1366,7 +1366,7 @@ func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID // Send start_miner which will fail because no manager is set. // The worker should send an error response via the connection. @@ -1405,9 +1405,9 @@ func TestWorker_HandleMessage_Stats_IntegrationViaWebSocket_Good(t *testing.T) { tp.connectClient(t) time.Sleep(50 * time.Millisecond) - serverID := tp.ServerNode.Identity().ID + serverID := tp.ServerNode.GetIdentity().ID - stats, err := controller.RemoteStats(serverID) + stats, err := controller.GetRemoteStats(serverID) if err != nil { t.Fatalf("RemoteStats failed: %v", err) } -- 2.45.3 From aa001ce2142080e65a9e6499ea0d2aa86574f081 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 05:18:20 +0000 Subject: [PATCH 29/60] refactor(node): rename websocket handler and clarify UEPS comments Co-Authored-By: Virgil --- node/controller_test.go | 2 +- node/integration_test.go | 6 +++--- node/transport.go | 6 +++--- node/transport_test.go | 8 ++++---- ueps/packet.go | 18 ++++++++++-------- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/node/controller_test.go b/node/controller_test.go index aa6ede8..c0881ef 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -50,7 +50,7 @@ func makeWorkerServer(t *testing.T) (*NodeManager, string, *Transport) { srv := NewTransport(nm, reg, cfg) mux := http.NewServeMux() - mux.HandleFunc(cfg.WebSocketPath, srv.handleWSUpgrade) + mux.HandleFunc(cfg.WebSocketPath, srv.handleWebSocketUpgrade) ts := httptest.NewServer(mux) u, _ := url.Parse(ts.URL) diff --git a/node/integration_test.go b/node/integration_test.go index f523457..4fd8f2e 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -82,7 +82,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // Start the worker transport behind httptest. mux := http.NewServeMux() - mux.HandleFunc(workerCfg.WebSocketPath, workerTransport.handleWSUpgrade) + mux.HandleFunc(workerCfg.WebSocketPath, workerTransport.handleWebSocketUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { controllerTransport.Stop() @@ -485,7 +485,7 @@ func TestIntegration_AllowlistHandshakeRejection_Bad(t *testing.T) { workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig()) mux := http.NewServeMux() - mux.HandleFunc("/ws", workerTransport.handleWSUpgrade) + mux.HandleFunc("/ws", workerTransport.handleWebSocketUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { workerTransport.Stop() @@ -529,7 +529,7 @@ func TestIntegration_AllowlistHandshakeAccepted_Good(t *testing.T) { worker.RegisterOnTransport() mux := http.NewServeMux() - mux.HandleFunc("/ws", workerTransport.handleWSUpgrade) + mux.HandleFunc("/ws", workerTransport.handleWebSocketUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { workerTransport.Stop() diff --git a/node/transport.go b/node/transport.go index d92dc29..a0755d0 100644 --- a/node/transport.go +++ b/node/transport.go @@ -313,7 +313,7 @@ func (t *Transport) agentUserAgent() string { // Start opens the WebSocket listener and background maintenance loops. func (t *Transport) Start() error { mux := http.NewServeMux() - mux.HandleFunc(t.config.webSocketPath(), t.handleWSUpgrade) + mux.HandleFunc(t.config.webSocketPath(), t.handleWebSocketUpgrade) listenAddress := t.config.listenAddress() @@ -533,8 +533,8 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { return t.connections[peerID] } -// handleWSUpgrade handles incoming WebSocket connections. -func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { +// handleWebSocketUpgrade handles incoming WebSocket connections. +func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") // Enforce the maximum connection limit, including pending handshakes. diff --git a/node/transport_test.go b/node/transport_test.go index 424da51..24969b1 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -72,7 +72,7 @@ func setupTestTransportPairWithConfig(t *testing.T, serverCfg, clientCfg Transpo // Use httptest.Server with the transport's WebSocket handler mux := http.NewServeMux() - mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWSUpgrade) + mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWebSocketUpgrade) ts := httptest.NewServer(mux) u, _ := url.Parse(ts.URL) @@ -253,7 +253,7 @@ func TestTransport_ConnectSendsAgentUserAgent_Good(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(serverCfg.WebSocketPath, func(w http.ResponseWriter, r *http.Request) { capturedUserAgent.Store(r.Header.Get("User-Agent")) - serverTransport.handleWSUpgrade(w, r) + serverTransport.handleWebSocketUpgrade(w, r) }) ts := httptest.NewServer(mux) @@ -482,7 +482,7 @@ func TestTransport_MaxConnectionsEnforcement_Good(t *testing.T) { serverTransport := NewTransport(serverNM, serverReg, serverCfg) mux := http.NewServeMux() - mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWSUpgrade) + mux.HandleFunc(serverCfg.WebSocketPath, serverTransport.handleWebSocketUpgrade) ts := httptest.NewServer(mux) t.Cleanup(func() { serverTransport.Stop() @@ -746,7 +746,7 @@ func TestTransport_NewTransport_DefaultMaxMessageSize_Good(t *testing.T) { if tr.config.MaxMessageSize != 0 { t.Errorf("config should preserve 0 value, got %d", tr.config.MaxMessageSize) } - // The actual default is applied at usage time (readLoop, handleWSUpgrade) + // The actual default is applied at usage time (readLoop, handleWebSocketUpgrade) } func TestTransport_ConnectedPeerCount_Good(t *testing.T) { diff --git a/ueps/packet.go b/ueps/packet.go index be9d440..45ed113 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -21,7 +21,7 @@ const ( TagPayload = 0xFF // The Data ) -// UEPSHeader represents the conscious routing metadata +// UEPSHeader represents the conscious routing metadata. // // header := UEPSHeader{IntentID: 0x01} type UEPSHeader struct { @@ -32,17 +32,17 @@ type UEPSHeader struct { ThreatScore uint16 // 0-65535 } -// PacketBuilder helps construct a signed UEPS frame +// PacketBuilder builds a signed UEPS frame from a concrete intent and payload. // -// builder := NewBuilder(0x01, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) type PacketBuilder struct { Header UEPSHeader Payload []byte } -// NewBuilder creates a packet context for a specific intent +// NewBuilder creates a packet builder for a specific intent and payload. // -// builder := NewBuilder(0x01, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { return &PacketBuilder{ Header: UEPSHeader{ @@ -56,7 +56,9 @@ func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { } } -// MarshalAndSign generates the final byte stream using the shared secret +// MarshalAndSign signs a packet with a shared secret. +// +// frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { buf := new(bytes.Buffer) @@ -106,8 +108,8 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return buf.Bytes(), nil } -// Helper to write a simple TLV. -// Now uses 2-byte big-endian length (uint16) to support up to 64KB payloads. +// writeTLV writes a single tag-length-value record with a 2-byte length prefix. +// It supports payloads up to 64KB. func writeTLV(w io.Writer, tag uint8, value []byte) error { // Check length constraint (2 byte length = max 65535 bytes) if len(value) > 65535 { -- 2.45.3 From 1c8592a5a0e407fe130d7fae1f613d5c565d5ada Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 05:32:08 +0000 Subject: [PATCH 30/60] refactor(node): align peer and handler names with AX Co-Authored-By: Virgil --- node/bench_test.go | 14 ++++---- node/controller.go | 50 ++++++++++++++-------------- node/controller_test.go | 8 ++--- node/dispatcher.go | 24 +++++++------- node/integration_test.go | 2 +- node/message.go | 8 ++--- node/peer.go | 46 +++++++++++++++---------- node/peer_test.go | 22 ++++++------ node/protocol.go | 4 +-- node/transport.go | 4 +-- node/worker.go | 72 ++++++++++++++++++++-------------------- 11 files changed, 132 insertions(+), 122 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index b6872b8..f96cefb 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -206,13 +206,13 @@ func BenchmarkPeerScoring(b *testing.B) { // Add 50 peers with varied metrics for i := range 50 { peer := &Peer{ - ID: testJoinPath("peer", string(rune('A'+i%26)), string(rune('0'+i/26))), - Name: "peer", - PingMS: float64(i*10 + 5), - Hops: i%5 + 1, - GeoKM: float64(i * 100), - Score: float64(50 + i%50), - AddedAt: time.Now(), + ID: testJoinPath("peer", string(rune('A'+i%26)), string(rune('0'+i/26))), + Name: "peer", + PingMilliseconds: float64(i*10 + 5), + Hops: i%5 + 1, + GeographicKilometres: float64(i * 100), + Score: float64(50 + i%50), + AddedAt: time.Now(), } // Bypass AddPeer's duplicate check by adding directly reg.mu.Lock() diff --git a/node/controller.go b/node/controller.go index 2bec4d5..a683738 100644 --- a/node/controller.go +++ b/node/controller.go @@ -41,34 +41,34 @@ func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transpo } // handleResponse processes incoming replies and routes them to the waiting request. -func (c *Controller) handleResponse(_ *PeerConnection, msg *Message) { - if msg.ReplyTo == "" { +func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { + if message.ReplyTo == "" { return // Not a response, let worker handle it } c.mutex.Lock() - responseChannel, hasPendingRequest := c.pendingRequests[msg.ReplyTo] + responseChannel, hasPendingRequest := c.pendingRequests[message.ReplyTo] if hasPendingRequest { - delete(c.pendingRequests, msg.ReplyTo) + delete(c.pendingRequests, message.ReplyTo) } c.mutex.Unlock() if hasPendingRequest && responseChannel != nil { select { - case responseChannel <- msg: + case responseChannel <- message: default: // Late duplicate response; drop it. } } } -// sendRequest registers a temporary response channel, sends msg, and waits +// sendRequest registers a temporary response channel, sends message, and waits // for the matching reply or timeout. // // The response channel is intentionally never closed. Removing it from the // pending map is enough to stop future routing, and it avoids a late-response // close/send race after the caller has already timed out. -func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Duration) (*Message, error) { +func (c *Controller) sendRequest(peerID string, message *Message, timeout time.Duration) (*Message, error) { resolvedPeerID := peerID // Auto-connect if not already connected @@ -84,26 +84,26 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat // Use the real peer ID after handshake (it may have changed) resolvedPeerID = conn.Peer.ID // Update the message destination - msg.To = resolvedPeerID + message.To = resolvedPeerID } // Create response channel responseChannel := make(chan *Message, 1) c.mutex.Lock() - c.pendingRequests[msg.ID] = responseChannel + c.pendingRequests[message.ID] = responseChannel c.mutex.Unlock() // Clean up on exit. Deleting the pending entry is enough because // handleResponse only routes through the map. defer func() { c.mutex.Lock() - delete(c.pendingRequests, msg.ID) + delete(c.pendingRequests, message.ID) c.mutex.Unlock() }() // Send the message - if err := c.transport.Send(resolvedPeerID, msg); err != nil { + if err := c.transport.Send(resolvedPeerID, message); err != nil { return nil, core.E("Controller.sendRequest", "failed to send message", err) } @@ -128,12 +128,12 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return nil, ErrorIdentityNotInitialized } - msg, err := NewMessage(MessageGetStats, identity.ID, peerID, nil) + requestMessage, err := NewMessage(MessageGetStats, identity.ID, peerID, nil) if err != nil { return nil, core.E("Controller.GetRemoteStats", "failed to create message", err) } - response, err := c.sendRequest(peerID, msg, 10*time.Second) + response, err := c.sendRequest(peerID, requestMessage, 10*time.Second) if err != nil { return nil, err } @@ -165,12 +165,12 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi Config: configOverride, } - msg, err := NewMessage(MessageStartMiner, identity.ID, peerID, payload) + requestMessage, err := NewMessage(MessageStartMiner, identity.ID, peerID, payload) if err != nil { return core.E("Controller.StartRemoteMiner", "failed to create message", err) } - response, err := c.sendRequest(peerID, msg, 30*time.Second) + response, err := c.sendRequest(peerID, requestMessage, 30*time.Second) if err != nil { return err } @@ -200,12 +200,12 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { MinerName: minerName, } - msg, err := NewMessage(MessageStopMiner, identity.ID, peerID, payload) + requestMessage, err := NewMessage(MessageStopMiner, identity.ID, peerID, payload) if err != nil { return core.E("Controller.StopRemoteMiner", "failed to create message", err) } - response, err := c.sendRequest(peerID, msg, 30*time.Second) + response, err := c.sendRequest(peerID, requestMessage, 30*time.Second) if err != nil { return err } @@ -236,12 +236,12 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin Lines: lines, } - msg, err := NewMessage(MessageGetLogs, identity.ID, peerID, payload) + requestMessage, err := NewMessage(MessageGetLogs, identity.ID, peerID, payload) if err != nil { return nil, core.E("Controller.GetRemoteLogs", "failed to create message", err) } - response, err := c.sendRequest(peerID, msg, 10*time.Second) + response, err := c.sendRequest(peerID, requestMessage, 10*time.Second) if err != nil { return nil, err } @@ -287,7 +287,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { // PingPeer sends a ping to a peer and refreshes that peer's metrics. // -// rttMS, err := controller.PingPeer("worker-1") +// rttMilliseconds, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -299,12 +299,12 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { SentAt: sentAt.UnixMilli(), } - msg, err := NewMessage(MessagePing, identity.ID, peerID, payload) + requestMessage, err := NewMessage(MessagePing, identity.ID, peerID, payload) if err != nil { return 0, core.E("Controller.PingPeer", "failed to create message", err) } - response, err := c.sendRequest(peerID, msg, 5*time.Second) + response, err := c.sendRequest(peerID, requestMessage, 5*time.Second) if err != nil { return 0, err } @@ -313,13 +313,13 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return 0, err } - // Calculate round-trip time - rtt := time.Since(sentAt).Seconds() * 1000 // Convert to ms + // Calculate round-trip time in milliseconds. + rtt := time.Since(sentAt).Seconds() * 1000 // Update peer metrics peer := c.peerRegistry.GetPeer(peerID) if peer != nil { - c.peerRegistry.UpdateMetrics(peerID, rtt, peer.GeoKM, peer.Hops) + c.peerRegistry.UpdateMetrics(peerID, rtt, peer.GeographicKilometres, peer.Hops) } return rtt, nil diff --git a/node/controller_test.go b/node/controller_test.go index c0881ef..dd05231 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -199,7 +199,7 @@ func TestController_PingPeerRTT_Good(t *testing.T) { // Record initial peer metrics. peerBefore := tp.ClientReg.GetPeer(serverID) require.NotNil(t, peerBefore, "server peer should exist in the client registry") - initialPingMS := peerBefore.PingMS + initialPingMilliseconds := peerBefore.PingMilliseconds // Send a ping. rtt, err := controller.PingPeer(serverID) @@ -210,9 +210,9 @@ func TestController_PingPeerRTT_Good(t *testing.T) { // Verify the peer registry was updated with the measured latency. peerAfter := tp.ClientReg.GetPeer(serverID) require.NotNil(t, peerAfter, "server peer should still exist after ping") - assert.NotEqual(t, initialPingMS, peerAfter.PingMS, - "PingMS should be updated after a successful ping") - assert.Greater(t, peerAfter.PingMS, 0.0, "PingMS should be positive") + assert.NotEqual(t, initialPingMilliseconds, peerAfter.PingMilliseconds, + "PingMilliseconds should be updated after a successful ping") + assert.Greater(t, peerAfter.PingMilliseconds, 0.0, "PingMilliseconds should be positive") } func TestController_ConcurrentRequests_Ugly(t *testing.T) { diff --git a/node/dispatcher.go b/node/dispatcher.go index 85d1110..ac78c49 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -29,8 +29,8 @@ const ( // IntentHandler processes a UEPS packet that has been routed by intent. // Implementations receive the fully parsed and HMAC-verified packet. // -// var handler IntentHandler = func(pkt *ueps.ParsedPacket) error { return nil } -type IntentHandler func(pkt *ueps.ParsedPacket) error +// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } +type IntentHandler func(packet *ueps.ParsedPacket) error // Dispatcher routes verified UEPS packets to registered intent handlers. // It enforces a threat circuit breaker before routing: any packet whose @@ -107,36 +107,36 @@ func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { // - Returns nil on successful delivery to a handler, or any error the // handler itself returns. // - A nil packet returns ErrorNilPacket immediately. -func (d *Dispatcher) Dispatch(pkt *ueps.ParsedPacket) error { - if pkt == nil { +func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { + if packet == nil { return ErrorNilPacket } // 1. Threat circuit breaker (L5 guard) - if pkt.Header.ThreatScore > ThreatScoreThreshold { + if packet.Header.ThreatScore > ThreatScoreThreshold { d.log.Warn("packet dropped: threat score exceeds safety threshold", logging.Fields{ - "threat_score": pkt.Header.ThreatScore, + "threat_score": packet.Header.ThreatScore, "threshold": ThreatScoreThreshold, - "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), - "version": pkt.Header.Version, + "intent_id": core.Sprintf("0x%02X", packet.Header.IntentID), + "version": packet.Header.Version, }) return ErrorThreatScoreExceeded } // 2. Intent routing (L9 semantic) d.mu.RLock() - handler, exists := d.handlers[pkt.Header.IntentID] + handler, exists := d.handlers[packet.Header.IntentID] d.mu.RUnlock() if !exists { d.log.Warn("packet dropped: unknown intent", logging.Fields{ - "intent_id": core.Sprintf("0x%02X", pkt.Header.IntentID), - "version": pkt.Header.Version, + "intent_id": core.Sprintf("0x%02X", packet.Header.IntentID), + "version": packet.Header.Version, }) return ErrorUnknownIntent } - return handler(pkt) + return handler(packet) } // Sentinel errors returned by Dispatch. diff --git a/node/integration_test.go b/node/integration_test.go index 4fd8f2e..bd45880 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -138,7 +138,7 @@ func TestIntegration_FullNodeLifecycle_Good(t *testing.T) { // Verify registry metrics were updated. peerAfterPing := controllerReg.GetPeer(serverPeerID) require.NotNil(t, peerAfterPing) - assert.Greater(t, peerAfterPing.PingMS, 0.0, "PingMS should be updated") + assert.Greater(t, peerAfterPing.PingMilliseconds, 0.0, "PingMilliseconds should be updated") // ---------------------------------------------------------------- // Step 5: Encrypted message exchange — RemoteStats diff --git a/node/message.go b/node/message.go index 8b7f37c..4931d8f 100644 --- a/node/message.go +++ b/node/message.go @@ -297,15 +297,15 @@ const ( // NewErrorMessage builds an error response message for an existing request. // -// msg, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") +// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { - msg, err := NewMessage(MessageError, from, to, ErrorPayload{ + errorMessage, err := NewMessage(MessageError, from, to, ErrorPayload{ Code: code, Message: message, }) if err != nil { return nil, err } - msg.ReplyTo = replyTo - return msg, nil + errorMessage.ReplyTo = replyTo + return errorMessage, nil } diff --git a/node/peer.go b/node/peer.go index 8d7458a..7776f7d 100644 --- a/node/peer.go +++ b/node/peer.go @@ -17,7 +17,14 @@ import ( // Peer represents a known remote node. // -// peer := &Peer{ID: "worker-1", Address: "127.0.0.1:9101"} +// peer := &Peer{ +// ID: "worker-1", +// Name: "Worker 1", +// Address: "127.0.0.1:9101", +// PingMilliseconds: 42.5, +// GeographicKilometres: 100, +// Score: 80, +// } type Peer struct { ID string `json:"id"` Name string `json:"name"` @@ -28,10 +35,10 @@ type Peer struct { LastSeen time.Time `json:"lastSeen"` // Poindexter metrics (updated dynamically) - PingMS float64 `json:"pingMs"` // Latency in milliseconds - Hops int `json:"hops"` // Network hop count - GeoKM float64 `json:"geoKm"` // Geographic distance in kilometers - Score float64 `json:"score"` // Reliability score 0-100 + PingMilliseconds float64 `json:"pingMs"` // Latency in milliseconds + Hops int `json:"hops"` // Network hop count + GeographicKilometres float64 `json:"geoKm"` // Geographic distance in kilometres + Score float64 `json:"score"` // Reliability score 0-100 // Connection state (not persisted) Connected bool `json:"-"` @@ -111,13 +118,13 @@ type PeerRegistry struct { saveMutex sync.Mutex // Protects pending save state } -// Dimension weights for peer selection -// Lower ping, hops, geo are better; higher score is better +// Dimension weights for peer selection. +// Lower ping, hops, and geographic distance are better; higher score is better. var ( - pingWeight = 1.0 - hopsWeight = 0.7 - geoWeight = 0.2 - scoreWeight = 1.2 + pingWeight = 1.0 + hopsWeight = 0.7 + geographicWeight = 0.2 + scoreWeight = 1.2 ) // NewPeerRegistry loads the default peer registry. @@ -362,8 +369,11 @@ func (r *PeerRegistry) Peers() iter.Seq[*Peer] { } // UpdateMetrics updates a peer's performance metrics. +// +// registry.UpdateMetrics("worker-1", 42.5, 100, 3) +// // Note: Persistence is debounced. Call Close() to flush before shutdown. -func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geoKilometres float64, hopCount int) error { +func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilometres float64, hopCount int) error { r.mu.Lock() peer, exists := r.peers[id] @@ -372,8 +382,8 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geoKilometres return core.E("PeerRegistry.UpdateMetrics", "peer "+id+" not found", nil) } - peer.PingMS = pingMilliseconds - peer.GeoKM = geoKilometres + peer.PingMilliseconds = pingMilliseconds + peer.GeographicKilometres = geographicKilometres peer.Hops = hopCount peer.LastSeen = time.Now() @@ -533,7 +543,7 @@ func (r *PeerRegistry) SelectOptimalPeer() *Peer { return nil } - // Target: ideal peer (0 ping, 0 hops, 0 geo, 100 score) + // Target: ideal peer (0 ping, 0 hops, 0 geographic distance, 100 score) // Score is inverted (100 - score) so lower is better in the tree target := []float64{0, 0, 0, 0} @@ -618,14 +628,14 @@ func (r *PeerRegistry) rebuildKDTree() { points := make([]poindexter.KDPoint[string], 0, len(r.peers)) for _, peer := range r.peers { - // Build 4D point with weighted, normalized values + // Build a 4D point with weighted, normalised values. // Invert score so that higher score = lower value (better) point := poindexter.KDPoint[string]{ ID: peer.ID, Coords: []float64{ - peer.PingMS * pingWeight, + peer.PingMilliseconds * pingWeight, float64(peer.Hops) * hopsWeight, - peer.GeoKM * geoWeight, + peer.GeographicKilometres * geographicWeight, (100 - peer.Score) * scoreWeight, // Invert score }, Value: peer.ID, diff --git a/node/peer_test.go b/node/peer_test.go index 6a79c79..b10979c 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -177,11 +177,11 @@ func TestPeer_Registry_UpdateMetrics_Good(t *testing.T) { if updated == nil { t.Fatal("expected peer to exist") } - if updated.PingMS != 50.5 { - t.Errorf("expected ping 50.5, got %f", updated.PingMS) + if updated.PingMilliseconds != 50.5 { + t.Errorf("expected ping 50.5, got %f", updated.PingMilliseconds) } - if updated.GeoKM != 100.2 { - t.Errorf("expected geo 100.2, got %f", updated.GeoKM) + if updated.GeographicKilometres != 100.2 { + t.Errorf("expected geographic distance 100.2, got %f", updated.GeographicKilometres) } if updated.Hops != 3 { t.Errorf("expected hops 3, got %d", updated.Hops) @@ -306,9 +306,9 @@ func TestPeer_Registry_SelectOptimalPeer_Good(t *testing.T) { // Add peers with different metrics peers := []*Peer{ - {ID: "opt-1", Name: "Slow Peer", PingMS: 200, Hops: 5, GeoKM: 1000, Score: 50}, - {ID: "opt-2", Name: "Fast Peer", PingMS: 10, Hops: 1, GeoKM: 50, Score: 90}, - {ID: "opt-3", Name: "Medium Peer", PingMS: 50, Hops: 2, GeoKM: 200, Score: 70}, + {ID: "opt-1", Name: "Slow Peer", PingMilliseconds: 200, Hops: 5, GeographicKilometres: 1000, Score: 50}, + {ID: "opt-2", Name: "Fast Peer", PingMilliseconds: 10, Hops: 1, GeographicKilometres: 50, Score: 90}, + {ID: "opt-3", Name: "Medium Peer", PingMilliseconds: 50, Hops: 2, GeographicKilometres: 200, Score: 70}, } for _, p := range peers { @@ -331,10 +331,10 @@ func TestPeer_Registry_SelectNearestPeers_Good(t *testing.T) { defer cleanup() peers := []*Peer{ - {ID: "near-1", Name: "Peer 1", PingMS: 100, Score: 50}, - {ID: "near-2", Name: "Peer 2", PingMS: 10, Score: 90}, - {ID: "near-3", Name: "Peer 3", PingMS: 50, Score: 70}, - {ID: "near-4", Name: "Peer 4", PingMS: 200, Score: 30}, + {ID: "near-1", Name: "Peer 1", PingMilliseconds: 100, Score: 50}, + {ID: "near-2", Name: "Peer 2", PingMilliseconds: 10, Score: 90}, + {ID: "near-3", Name: "Peer 3", PingMilliseconds: 50, Score: 70}, + {ID: "near-4", Name: "Peer 4", PingMilliseconds: 200, Score: 30}, } for _, p := range peers { diff --git a/node/protocol.go b/node/protocol.go index bc11502..34f255d 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -68,14 +68,14 @@ var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. // -// err := ValidateResponse(msg, MessageStats) +// err := ValidateResponse(message, MessageStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } // ParseResponse is a convenience function using the default handler. // -// err := ParseResponse(msg, MessageStats, &stats) +// err := ParseResponse(message, MessageStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } diff --git a/node/transport.go b/node/transport.go index a0755d0..55871f5 100644 --- a/node/transport.go +++ b/node/transport.go @@ -94,8 +94,8 @@ func (c TransportConfig) maximumConnections() int { // MessageHandler processes incoming messages. // -// var handler MessageHandler = func(conn *PeerConnection, msg *Message) {} -type MessageHandler func(conn *PeerConnection, msg *Message) +// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} +type MessageHandler func(peerConnection *PeerConnection, message *Message) // MessageDeduplicator tracks recent message IDs to prevent duplicate processing. // diff --git a/node/worker.go b/node/worker.go index 8997327..9c0669d 100644 --- a/node/worker.go +++ b/node/worker.go @@ -79,24 +79,24 @@ func (w *Worker) SetProfileManager(manager ProfileManager) { // HandleMessage routes an incoming message to the correct worker handler. // -// worker.HandleMessage(conn, msg) -func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { +// worker.HandleMessage(peerConnection, message) +func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) { var response *Message var err error - switch msg.Type { + switch message.Type { case MessagePing: - response, err = w.handlePing(msg) + response, err = w.handlePing(message) case MessageGetStats: - response, err = w.handleStats(msg) + response, err = w.handleStats(message) case MessageStartMiner: - response, err = w.handleStartMiner(msg) + response, err = w.handleStartMiner(message) case MessageStopMiner: - response, err = w.handleStopMiner(msg) + response, err = w.handleStopMiner(message) case MessageGetLogs: - response, err = w.handleLogs(msg) + response, err = w.handleLogs(message) case MessageDeploy: - response, err = w.handleDeploy(conn, msg) + response, err = w.handleDeploy(peerConnection, message) default: // Unknown message type - ignore or send error return @@ -108,19 +108,19 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { if identity != nil { errMsg, _ := NewErrorMessage( identity.ID, - msg.From, + message.From, ErrorCodeOperationFailed, err.Error(), - msg.ID, + message.ID, ) - conn.Send(errMsg) + peerConnection.Send(errMsg) } return } if response != nil { - logging.Debug("sending response", logging.Fields{"type": response.Type, "to": msg.From}) - if err := conn.Send(response); err != nil { + logging.Debug("sending response", logging.Fields{"type": response.Type, "to": message.From}) + if err := peerConnection.Send(response); err != nil { logging.Error("failed to send response", logging.Fields{"error": err}) } else { logging.Debug("response sent successfully") @@ -129,9 +129,9 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { } // handlePing responds to ping requests. -func (w *Worker) handlePing(msg *Message) (*Message, error) { +func (w *Worker) handlePing(message *Message) (*Message, error) { var ping PingPayload - if err := msg.ParsePayload(&ping); err != nil { + if err := message.ParsePayload(&ping); err != nil { return nil, core.E("Worker.handlePing", "invalid ping payload", err) } @@ -140,11 +140,11 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { ReceivedAt: time.Now().UnixMilli(), } - return msg.Reply(MessagePong, pong) + return message.Reply(MessagePong, pong) } // handleStats responds with current miner statistics. -func (w *Worker) handleStats(msg *Message) (*Message, error) { +func (w *Worker) handleStats(message *Message) (*Message, error) { identity := w.nodeManager.GetIdentity() if identity == nil { return nil, ErrorIdentityNotInitialized @@ -172,7 +172,7 @@ func (w *Worker) handleStats(msg *Message) (*Message, error) { } } - return msg.Reply(MessageStats, stats) + return message.Reply(MessageStats, stats) } // convertMinerStats converts miner stats to the protocol format. @@ -208,13 +208,13 @@ func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { } // handleStartMiner starts a miner with the given profile. -func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { +func (w *Worker) handleStartMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured } var payload StartMinerPayload - if err := msg.ParsePayload(&payload); err != nil { + if err := message.ParsePayload(&payload); err != nil { return nil, core.E("Worker.handleStartMiner", "invalid start miner payload", err) } @@ -244,24 +244,24 @@ func (w *Worker) handleStartMiner(msg *Message) (*Message, error) { Success: false, Error: err.Error(), } - return msg.Reply(MessageMinerAck, ack) + return message.Reply(MessageMinerAck, ack) } ack := MinerAckPayload{ Success: true, MinerName: miner.GetName(), } - return msg.Reply(MessageMinerAck, ack) + return message.Reply(MessageMinerAck, ack) } // handleStopMiner stops a running miner. -func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { +func (w *Worker) handleStopMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured } var payload StopMinerPayload - if err := msg.ParsePayload(&payload); err != nil { + if err := message.ParsePayload(&payload); err != nil { return nil, core.E("Worker.handleStopMiner", "invalid stop miner payload", err) } @@ -274,17 +274,17 @@ func (w *Worker) handleStopMiner(msg *Message) (*Message, error) { ack.Error = err.Error() } - return msg.Reply(MessageMinerAck, ack) + return message.Reply(MessageMinerAck, ack) } // handleLogs returns console logs from a miner. -func (w *Worker) handleLogs(msg *Message) (*Message, error) { +func (w *Worker) handleLogs(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured } var payload LogsRequestPayload - if err := msg.ParsePayload(&payload); err != nil { + if err := message.ParsePayload(&payload); err != nil { return nil, core.E("Worker.handleLogs", "invalid logs payload", err) } @@ -307,13 +307,13 @@ func (w *Worker) handleLogs(msg *Message) (*Message, error) { HasMore: len(lines) >= payload.Lines, } - return msg.Reply(MessageLogs, logs) + return message.Reply(MessageLogs, logs) } // handleDeploy handles deployment of profiles or miner bundles. -func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, error) { +func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) (*Message, error) { var payload DeployPayload - if err := msg.ParsePayload(&payload); err != nil { + if err := message.ParsePayload(&payload); err != nil { return nil, core.E("Worker.handleDeploy", "invalid deploy payload", err) } @@ -327,8 +327,8 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err // Use shared secret as password (base64 encoded) password := "" - if conn != nil && len(conn.SharedSecret) > 0 { - password = base64.StdEncoding.EncodeToString(conn.SharedSecret) + if peerConnection != nil && len(peerConnection.SharedSecret) > 0 { + password = base64.StdEncoding.EncodeToString(peerConnection.SharedSecret) } switch bundle.Type { @@ -355,14 +355,14 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err Name: payload.Name, Error: err.Error(), } - return msg.Reply(MessageDeployAck, ack) + return message.Reply(MessageDeployAck, ack) } ack := DeployAckPayload{ Success: true, Name: payload.Name, } - return msg.Reply(MessageDeployAck, ack) + return message.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: // Determine the installation directory under the configured deployment @@ -406,7 +406,7 @@ func (w *Worker) handleDeploy(conn *PeerConnection, msg *Message) (*Message, err "miner_path": minerPath, }) - return msg.Reply(MessageDeployAck, ack) + return message.Reply(MessageDeployAck, ack) default: return nil, core.E("Worker.handleDeploy", "unknown bundle type: "+payload.BundleType, nil) -- 2.45.3 From 1badcf1877ce40d8b3bfedc0d83d3e636cf2a638 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 05:44:02 +0000 Subject: [PATCH 31/60] fix(transport): expose accepted peers before handshake ack Co-Authored-By: Virgil --- node/transport.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/node/transport.go b/node/transport.go index 55871f5..595f0c4 100644 --- a/node/transport.go +++ b/node/transport.go @@ -416,6 +416,8 @@ func (t *Transport) OnMessage(handler MessageHandler) { } // Connect dials a peer, completes the handshake, and starts the session loops. +// +// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -679,7 +681,6 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } - // Send handshake acknowledgment identity := t.nodeManager.GetIdentity() if identity == nil { conn.Close() @@ -711,16 +712,18 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques return } - if err := conn.WriteMessage(websocket.TextMessage, ackData); err != nil { - conn.Close() - return - } - - // Store connection + // Make the accepted connection visible before the client reads the ack. + // Connect() returns only after that read completes, so this keeps the + // server registry aligned with the caller's view of the handshake. t.mutex.Lock() t.connections[peer.ID] = pc t.mutex.Unlock() + if err := conn.WriteMessage(websocket.TextMessage, ackData); err != nil { + t.removeConnection(pc) + return + } + // Update registry t.peerRegistry.SetConnected(peer.ID, true) -- 2.45.3 From 5ed4451555bca6f0ae7641c5231ccb1abf5cef7b Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 31 Mar 2026 12:20:52 +0100 Subject: [PATCH 32/60] =?UTF-8?q?feat(ax):=20AX=20compliance=20sweep=20?= =?UTF-8?q?=E2=80=94=20comments=20as=20usage=20examples,=20remove=20linter?= =?UTF-8?q?-injected=20aliases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied AX principles 1, 2, and 9 across the node, dispatcher, transport, peer, worker, bundle, and logging packages: - Added usage-example comments to all public methods missing them (AX-2): dispatcher.RegisterHandler, Handlers; peer.AddPeer, UpdatePeer, RemovePeer, AllowPublicKey, RevokePublicKey, IsPublicKeyAllowed, IsPeerAllowed, RecordSuccess, RecordFailure, RecordTimeout, SelectOptimalPeer, SelectNearestPeers, Count, PeersByScore, AllowedPublicKeys, ListAllowedPublicKeys; transport.Start, Stop, Send, Connections, Broadcast, PeerConnection.Send, Close, GracefulClose - Removed redundant inline comments that restate code (bundle.go, transport.go, worker.go) - Added _Bad and _Ugly test categories to logging/logger_test.go to satisfy the TestFilename_Function_{Good,Bad,Ugly} naming convention (AX-10) - Removed all linter-injected short-form alias files (*_compat.go, *_alias_test.go) that violated AX-1 (Err* aliases, Uint64Val, WithComponent, GetLevel, GetGlobal, etc.) Co-Authored-By: Virgil --- logging/logger_test.go | 33 ++++++++++++++++++++++++++ node/bundle.go | 4 +--- node/dispatcher.go | 12 ++++++++-- node/errors.go | 1 + node/peer.go | 54 ++++++++++++++++++++++++++++++++++-------- node/transport.go | 26 ++++++++++++++++---- node/worker.go | 3 +-- 7 files changed, 111 insertions(+), 22 deletions(-) diff --git a/logging/logger_test.go b/logging/logger_test.go index 11ba5cb..08c48a9 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -2,6 +2,7 @@ package logging import ( "bytes" + "sync" "testing" core "dappco.re/go/core" @@ -266,3 +267,35 @@ func TestLogger_MergeFields_Good(t *testing.T) { t.Error("Later fields should override earlier ones") } } + +func TestLogger_ParseLevel_Bad(t *testing.T) { + _, err := ParseLevel("bogus") + if err == nil { + t.Error("ParseLevel should return an error for an unrecognised level string") + } +} + +func TestLogger_ConcurrentWrite_Ugly(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelDebug, + }) + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := range goroutines { + go func(n int) { + defer wg.Done() + logger.Infof("concurrent message %d", n) + }(i) + } + + wg.Wait() + // Only assert no panics / races occurred; output ordering is non-deterministic. + if buf.Len() == 0 { + t.Error("expected concurrent log writes to produce output") + } +} diff --git a/node/bundle.go b/node/bundle.go index 03a78ed..de52eec 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -224,11 +224,9 @@ func createTarball(files map[string][]byte) ([]byte, error) { var buf bytes.Buffer tarWriter := tar.NewWriter(&buf) - // Track directories we've created createdDirectories := make(map[string]bool) for name, content := range files { - // Create parent directories if needed dir := core.PathDir(name) if dir != "." && !createdDirectories[dir] { header := &tar.Header{ @@ -242,7 +240,7 @@ func createTarball(files map[string][]byte) ([]byte, error) { createdDirectories[dir] = true } - // Determine file mode (executable for binaries in miners/) + // Binaries in miners/ and non-JSON content get executable permissions. mode := int64(0644) if core.PathDir(name) == "miners" || !isJSON(content) { mode = 0755 diff --git a/node/dispatcher.go b/node/dispatcher.go index ac78c49..126e3b2 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -71,8 +71,11 @@ func NewDispatcher() *Dispatcher { } // RegisterHandler associates an IntentHandler with a specific IntentID. -// Calling RegisterHandler with an IntentID that already has a handler will -// replace the previous handler. +// Replacing an existing handler is allowed — the new handler takes effect immediately. +// +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { +// return processComputeJob(packet.Payload) +// }) func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { d.mu.Lock() defer d.mu.Unlock() @@ -83,6 +86,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { } // Handlers returns an iterator over all registered intent handlers. +// +// for intentID, handler := range dispatcher.Handlers() { +// log.Printf("registered intent 0x%02X", intentID) +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() @@ -152,3 +159,4 @@ var ( // ErrorNilPacket is returned when a nil packet is passed to Dispatch. ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) + diff --git a/node/errors.go b/node/errors.go index 7d29af4..f660be1 100644 --- a/node/errors.go +++ b/node/errors.go @@ -12,3 +12,4 @@ var ( // attempted but no MinerManager has been set on the Worker. ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) + diff --git a/node/peer.go b/node/peer.go index 7776f7d..aeafe82 100644 --- a/node/peer.go +++ b/node/peer.go @@ -187,6 +187,8 @@ func (r *PeerRegistry) GetAuthMode() PeerAuthMode { } // AllowPublicKey adds a public key to the allowlist. +// +// registry.AllowPublicKey(peer.PublicKey) func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -195,6 +197,8 @@ func (r *PeerRegistry) AllowPublicKey(publicKey string) { } // RevokePublicKey removes a public key from the allowlist. +// +// registry.RevokePublicKey(peer.PublicKey) func (r *PeerRegistry) RevokePublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -203,6 +207,8 @@ func (r *PeerRegistry) RevokePublicKey(publicKey string) { } // IsPublicKeyAllowed checks if a public key is in the allowlist. +// +// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() @@ -210,9 +216,10 @@ func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool { } // IsPeerAllowed checks if a peer is allowed to connect based on auth mode. -// Returns true if: -// - AuthMode is Open (allow all) -// - AuthMode is Allowlist AND (peer is pre-registered OR public key is allowlisted) +// Returns true when AuthMode is Open (all allowed), or when Allowlist mode is active +// and the peer is pre-registered or its public key is in the allowlist. +// +// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { r.allowedPublicKeyMu.RLock() authMode := r.authMode @@ -238,11 +245,17 @@ func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { } // ListAllowedPublicKeys returns all allowlisted public keys. +// +// keys := registry.ListAllowedPublicKeys() func (r *PeerRegistry) ListAllowedPublicKeys() []string { return slices.Collect(r.AllowedPublicKeys()) } // AllowedPublicKeys returns an iterator over all allowlisted public keys. +// +// for key := range registry.AllowedPublicKeys() { +// log.Printf("allowed: %s", key[:16]) +// } func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { return func(yield func(string) bool) { r.allowedPublicKeyMu.RLock() @@ -257,8 +270,9 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } // AddPeer adds a new peer to the registry. -// Note: Persistence is debounced (writes batched every 5s). Call Close() to ensure -// all changes are flushed to disk before shutdown. +// Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. +// +// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { r.mu.Lock() @@ -295,7 +309,9 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { } // UpdatePeer updates an existing peer's information. -// Note: Persistence is debounced. Call Close() to flush before shutdown. +// Persistence is debounced. Call Close() to flush before shutdown. +// +// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { r.mu.Lock() @@ -313,7 +329,9 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { } // RemovePeer removes a peer from the registry. -// Note: Persistence is debounced. Call Close() to flush before shutdown. +// Persistence is debounced. Call Close() to flush before shutdown. +// +// err := registry.RemovePeer("worker-1") func (r *PeerRegistry) RemovePeer(id string) error { r.mu.Lock() @@ -442,6 +460,8 @@ const ( ) // RecordSuccess records a successful interaction with a peer, improving their score. +// +// registry.RecordSuccess("worker-1") func (r *PeerRegistry) RecordSuccess(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -457,6 +477,8 @@ func (r *PeerRegistry) RecordSuccess(id string) { } // RecordFailure records a failed interaction with a peer, reducing their score. +// +// registry.RecordFailure("worker-1") func (r *PeerRegistry) RecordFailure(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -478,6 +500,8 @@ func (r *PeerRegistry) RecordFailure(id string) { } // RecordTimeout records a timeout when communicating with a peer. +// +// registry.RecordTimeout("worker-1") func (r *PeerRegistry) RecordTimeout(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -522,6 +546,10 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { } // PeersByScore returns an iterator over peers sorted by score (highest first). +// +// for peer := range registry.PeersByScore() { +// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) +// } func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { peers := r.GetPeersByScore() @@ -533,8 +561,10 @@ func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { } } -// SelectOptimalPeer returns the best peer based on multi-factor optimization. -// Uses Poindexter KD-tree to find the peer closest to ideal metrics. +// SelectOptimalPeer returns the best peer based on multi-factor optimisation. +// Uses Poindexter KD-tree to find the peer closest to ideal metrics (low ping, low hops, high score). +// +// peer := registry.SelectOptimalPeer() func (r *PeerRegistry) SelectOptimalPeer() *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -561,7 +591,9 @@ func (r *PeerRegistry) SelectOptimalPeer() *Peer { return &peerCopy } -// SelectNearestPeers returns the n best peers based on multi-factor optimization. +// SelectNearestPeers returns the n best peers based on multi-factor optimisation. +// +// peers := registry.SelectNearestPeers(3) func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -612,6 +644,8 @@ func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { } // Count returns the number of registered peers. +// +// n := registry.Count() func (r *PeerRegistry) Count() int { r.mu.RLock() defer r.mu.RUnlock() diff --git a/node/transport.go b/node/transport.go index 595f0c4..4acdfff 100644 --- a/node/transport.go +++ b/node/transport.go @@ -311,6 +311,8 @@ func (t *Transport) agentUserAgent() string { } // Start opens the WebSocket listener and background maintenance loops. +// +// err := transport.Start() func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.webSocketPath(), t.handleWebSocketUpgrade) @@ -380,6 +382,8 @@ func (t *Transport) Start() error { } // Stop closes active connections and shuts the transport down cleanly. +// +// err := transport.Stop() func (t *Transport) Stop() error { t.cancelLifecycle() @@ -482,6 +486,8 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } // Send transmits an encrypted message to a connected peer. +// +// err := transport.Send("worker-1", message) func (t *Transport) Send(peerID string, msg *Message) error { t.mutex.RLock() pc, exists := t.connections[peerID] @@ -495,6 +501,10 @@ func (t *Transport) Send(peerID string, msg *Message) error { } // Connections returns an iterator over all active peer connections. +// +// for pc := range transport.Connections() { +// log.Printf("connected: %s", pc.Peer.ID) +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -509,13 +519,14 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } // Broadcast sends a message to every connected peer except the sender. -// The sender is identified by msg.From and excluded to prevent echo. +// The sender (msg.From) is excluded to prevent echo. +// +// err := transport.Broadcast(announcement) func (t *Transport) Broadcast(msg *Message) error { conns := slices.Collect(t.Connections()) var lastErr error for _, pc := range conns { - // Exclude sender from broadcast to prevent echo (P2P-MED-6) if pc.Peer != nil && pc.Peer.ID == msg.From { continue } @@ -963,26 +974,28 @@ func (t *Transport) removeConnection(pc *PeerConnection) { } // Send sends an encrypted message over the connection. +// +// err := peerConnection.Send(message) func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() - // Encrypt message using SMSG data, err := pc.transport.encryptMessage(msg, pc.SharedSecret) if err != nil { return err } - // Set write deadline to prevent blocking forever if err := pc.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { return core.E("PeerConnection.Send", "failed to set write deadline", err) } - defer pc.Conn.SetWriteDeadline(time.Time{}) // Reset deadline after send + defer pc.Conn.SetWriteDeadline(time.Time{}) return pc.Conn.WriteMessage(websocket.BinaryMessage, data) } // Close closes the connection. +// +// err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { @@ -1009,6 +1022,8 @@ const ( ) // GracefulClose sends a disconnect message before closing the connection. +// +// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { @@ -1083,3 +1098,4 @@ func (t *Transport) ConnectedPeerCount() int { defer t.mutex.RUnlock() return len(t.connections) } + diff --git a/node/worker.go b/node/worker.go index 9c0669d..711c16b 100644 --- a/node/worker.go +++ b/node/worker.go @@ -175,14 +175,13 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { return message.Reply(MessageStats, stats) } -// convertMinerStats converts miner stats to the protocol format. +// convertMinerStats converts a running miner's raw stats map to the wire protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ Name: miner.GetName(), Type: miner.GetType(), } - // Try to extract common fields from the stats if statsMap, ok := rawStats.(map[string]any); ok { if hashrate, ok := statsMap["hashrate"].(float64); ok { item.Hashrate = hashrate -- 2.45.3 From 85de30a5280197ebfe6562bc5482689d0a55d8d5 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 31 Mar 2026 12:25:22 +0100 Subject: [PATCH 33/60] feat(compat): implement spec-described aliases across all packages Add compatibility aliases documented in specs but missing from code: - node: NewNodeManagerWithPaths, NewPeerRegistryWithPath (deprecated constructor aliases), RegisterWithTransport (deprecated method alias), ConnectedPeers (count alias), GetLogsPayload (type alias) - levin: Short-form Value constructors (Uint64Val, StringVal, ObjectVal, etc.) matching spec naming - logging: WithComponent, GetLevel, GetGlobal (deprecated method aliases) - ueps: TagCurrentLay, TagTargetLay (short-form tag constant aliases) All aliases delegate to the canonical AX-compliant names. Tests cover every alias with round-trip verification where applicable. Co-Authored-By: Virgil --- logging/compat.go | 25 +++++++++++ logging/compat_test.go | 39 ++++++++++++++++++ node/compat.go | 46 +++++++++++++++++++++ node/compat_test.go | 57 +++++++++++++++++++++++++ node/levin/compat.go | 87 +++++++++++++++++++++++++++++++++++++++ node/levin/compat_test.go | 66 +++++++++++++++++++++++++++++ ueps/compat.go | 9 ++++ ueps/compat_test.go | 14 +++++++ 8 files changed, 343 insertions(+) create mode 100644 logging/compat.go create mode 100644 logging/compat_test.go create mode 100644 node/compat.go create mode 100644 node/compat_test.go create mode 100644 node/levin/compat.go create mode 100644 node/levin/compat_test.go create mode 100644 ueps/compat.go create mode 100644 ueps/compat_test.go diff --git a/logging/compat.go b/logging/compat.go new file mode 100644 index 0000000..83a624d --- /dev/null +++ b/logging/compat.go @@ -0,0 +1,25 @@ +package logging + +// WithComponent returns a new Logger scoped to one component. +// Deprecated: Use ComponentLogger instead. +// +// transportLogger := logger.WithComponent("transport") +func (l *Logger) WithComponent(component string) *Logger { + return l.ComponentLogger(component) +} + +// GetLevel returns the current log level. +// Preferred over Level for AX naming consistency. +// +// level := logger.GetLevel() +func (l *Logger) GetLevel() Level { + return l.Level() +} + +// GetGlobal returns the global logger instance. +// Deprecated: Use Global instead. +// +// logger := GetGlobal() +func GetGlobal() *Logger { + return Global() +} diff --git a/logging/compat_test.go b/logging/compat_test.go new file mode 100644 index 0000000..1310c17 --- /dev/null +++ b/logging/compat_test.go @@ -0,0 +1,39 @@ +package logging + +import ( + "bytes" + "testing" + + core "dappco.re/go/core" +) + +func TestGetLevel_Good(t *testing.T) { + logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) + if logger.GetLevel() != LevelWarn { + t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) + } + if logger.GetLevel() != logger.Level() { + t.Error("GetLevel() and Level() should return the same value") + } +} + +func TestWithComponent_Good(t *testing.T) { + var buf bytes.Buffer + parent := New(Config{Output: &buf, Level: LevelInfo}) + child := parent.WithComponent("TestChild") + child.Info("test message") + if !core.Contains(buf.String(), "[TestChild]") { + t.Error("WithComponent should set the component name") + } +} + +func TestGetGlobal_Good(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{Output: &buf, Level: LevelInfo}) + SetGlobal(logger) + got := GetGlobal() + if got != Global() { + t.Error("GetGlobal() and Global() should return the same logger") + } + SetGlobal(New(DefaultConfig())) +} diff --git a/node/compat.go b/node/compat.go new file mode 100644 index 0000000..fce12ad --- /dev/null +++ b/node/compat.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// This file provides deprecated compatibility aliases and type aliases +// for spec compatibility. Canonical names are defined in their respective +// source files; these aliases exist so that code written against the spec +// documentation continues to compile. + +package node + +// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. +// Deprecated: Use NewNodeManagerFromPaths instead. +// +// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { + return NewNodeManagerFromPaths(keyPath, configPath) +} + +// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. +// Deprecated: Use NewPeerRegistryFromPath instead. +// +// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") +func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { + return NewPeerRegistryFromPath(peersPath) +} + +// RegisterWithTransport installs the worker message handler on the transport. +// Deprecated: Use RegisterOnTransport instead. +// +// worker.RegisterWithTransport() +func (w *Worker) RegisterWithTransport() { + w.RegisterOnTransport() +} + +// ConnectedPeers returns the number of connected peers. +// Deprecated: Use ConnectedPeerCount instead. +// +// count := transport.ConnectedPeers() +func (t *Transport) ConnectedPeers() int { + return t.ConnectedPeerCount() +} + +// GetLogsPayload is an alias for LogsRequestPayload. +// Provided for spec compatibility. +// +// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} +type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go new file mode 100644 index 0000000..9b197aa --- /dev/null +++ b/node/compat_test.go @@ -0,0 +1,57 @@ +package node + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewNodeManagerWithPaths_Good(t *testing.T) { + keyPath := t.TempDir() + "/private.key" + configPath := t.TempDir() + "/node.json" + + nm, err := NewNodeManagerWithPaths(keyPath, configPath) + require.NoError(t, err) + assert.NotNil(t, nm) + assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") +} + +func TestNewPeerRegistryWithPath_Good(t *testing.T) { + peersPath := t.TempDir() + "/peers.json" + + pr, err := NewPeerRegistryWithPath(peersPath) + require.NoError(t, err) + assert.NotNil(t, pr) + assert.Equal(t, 0, pr.Count()) + pr.Close() +} + +func TestGetLogsPayload_Good(t *testing.T) { + var payload GetLogsPayload + payload.MinerName = "xmrig-0" + payload.Lines = 100 + payload.Since = 1234567890 + + var request LogsRequestPayload = payload + assert.Equal(t, "xmrig-0", request.MinerName) + assert.Equal(t, 100, request.Lines) + assert.Equal(t, int64(1234567890), request.Since) +} + +func TestTransportConnectedPeers_Good(t *testing.T) { + keyPath := t.TempDir() + "/private.key" + configPath := t.TempDir() + "/node.json" + peersPath := t.TempDir() + "/peers.json" + + nm, err := NewNodeManagerFromPaths(keyPath, configPath) + require.NoError(t, err) + + pr, err := NewPeerRegistryFromPath(peersPath) + require.NoError(t, err) + defer pr.Close() + + transport := NewTransport(nm, pr, DefaultTransportConfig()) + assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) + assert.Equal(t, 0, transport.ConnectedPeers()) +} diff --git a/node/levin/compat.go b/node/levin/compat.go new file mode 100644 index 0000000..a88bdfb --- /dev/null +++ b/node/levin/compat.go @@ -0,0 +1,87 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +// This file provides short-form aliases for Value constructors, +// matching the naming used in the spec documentation. + +package levin + +// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. +// +// value := Uint64Val(42) +func Uint64Val(v uint64) Value { return Uint64Value(v) } + +// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. +// +// value := Uint32Val(42) +func Uint32Val(v uint32) Value { return Uint32Value(v) } + +// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. +// +// value := Uint16Val(42) +func Uint16Val(v uint16) Value { return Uint16Value(v) } + +// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. +// +// value := Uint8Val(42) +func Uint8Val(v uint8) Value { return Uint8Value(v) } + +// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. +// +// value := Int64Val(42) +func Int64Val(v int64) Value { return Int64Value(v) } + +// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. +// +// value := Int32Val(42) +func Int32Val(v int32) Value { return Int32Value(v) } + +// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. +// +// value := Int16Val(42) +func Int16Val(v int16) Value { return Int16Value(v) } + +// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. +// +// value := Int8Val(42) +func Int8Val(v int8) Value { return Int8Value(v) } + +// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. +// +// value := BoolVal(true) +func BoolVal(v bool) Value { return BoolValue(v) } + +// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. +// +// value := DoubleVal(3.14) +func DoubleVal(v float64) Value { return DoubleValue(v) } + +// StringVal creates a Value of TypeString. Short-form alias for StringValue. +// +// value := StringVal([]byte("hello")) +func StringVal(v []byte) Value { return StringValue(v) } + +// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. +// +// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) +func ObjectVal(s Section) Value { return ObjectValue(s) } + +// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. +// +// value := Uint64ArrayVal([]uint64{1, 2, 3}) +func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } + +// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. +// +// value := Uint32ArrayVal([]uint32{1, 2, 3}) +func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } + +// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. +// +// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) +func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } + +// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. +// +// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) +func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go new file mode 100644 index 0000000..72de5f4 --- /dev/null +++ b/node/levin/compat_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +package levin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShortFormAliases_Good(t *testing.T) { + assert.Equal(t, Uint64Value(42), Uint64Val(42)) + assert.Equal(t, Uint32Value(42), Uint32Val(42)) + assert.Equal(t, Uint16Value(42), Uint16Val(42)) + assert.Equal(t, Uint8Value(42), Uint8Val(42)) + assert.Equal(t, Int64Value(-42), Int64Val(-42)) + assert.Equal(t, Int32Value(-42), Int32Val(-42)) + assert.Equal(t, Int16Value(-42), Int16Val(-42)) + assert.Equal(t, Int8Value(-42), Int8Val(-42)) + assert.Equal(t, BoolValue(true), BoolVal(true)) + assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) + assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) + + section := Section{"key": StringValue([]byte("value"))} + assert.Equal(t, ObjectValue(section), ObjectVal(section)) + + assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) + assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) + assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) + assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) +} + +func TestShortFormAliasesRoundTrip_Good(t *testing.T) { + s := Section{ + "u64": Uint64Val(0xCAFEBABE), + "str": StringVal([]byte("hello")), + "flag": BoolVal(true), + "obj": ObjectVal(Section{"nested": Int32Val(42)}), + } + + data, err := EncodeStorage(s) + require.NoError(t, err) + + decoded, err := DecodeStorage(data) + require.NoError(t, err) + + u64, err := decoded["u64"].AsUint64() + require.NoError(t, err) + assert.Equal(t, uint64(0xCAFEBABE), u64) + + str, err := decoded["str"].AsString() + require.NoError(t, err) + assert.Equal(t, []byte("hello"), str) + + flag, err := decoded["flag"].AsBool() + require.NoError(t, err) + assert.True(t, flag) + + obj, err := decoded["obj"].AsSection() + require.NoError(t, err) + nested, err := obj["nested"].AsInt32() + require.NoError(t, err) + assert.Equal(t, int32(42), nested) +} diff --git a/ueps/compat.go b/ueps/compat.go new file mode 100644 index 0000000..39d715a --- /dev/null +++ b/ueps/compat.go @@ -0,0 +1,9 @@ +package ueps + +// Short-form tag aliases for spec compatibility. +const ( + // TagCurrentLay is a short-form alias for TagCurrentLayer. + TagCurrentLay = TagCurrentLayer + // TagTargetLay is a short-form alias for TagTargetLayer. + TagTargetLay = TagTargetLayer +) diff --git a/ueps/compat_test.go b/ueps/compat_test.go new file mode 100644 index 0000000..d57fc01 --- /dev/null +++ b/ueps/compat_test.go @@ -0,0 +1,14 @@ +package ueps + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTagAliases_Good(t *testing.T) { + assert.Equal(t, TagCurrentLayer, TagCurrentLay) + assert.Equal(t, TagTargetLayer, TagTargetLay) + assert.Equal(t, 0x02, TagCurrentLay) + assert.Equal(t, 0x03, TagTargetLay) +} -- 2.45.3 From aa182f5284815aa4e33f7e84caca1746a0170e18 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 31 Mar 2026 12:26:23 +0100 Subject: [PATCH 34/60] Revert "feat(compat): implement spec-described aliases across all packages" This reverts commit 85de30a5280197ebfe6562bc5482689d0a55d8d5. --- logging/compat.go | 25 ----------- logging/compat_test.go | 39 ------------------ node/compat.go | 46 --------------------- node/compat_test.go | 57 ------------------------- node/levin/compat.go | 87 --------------------------------------- node/levin/compat_test.go | 66 ----------------------------- ueps/compat.go | 9 ---- ueps/compat_test.go | 14 ------- 8 files changed, 343 deletions(-) delete mode 100644 logging/compat.go delete mode 100644 logging/compat_test.go delete mode 100644 node/compat.go delete mode 100644 node/compat_test.go delete mode 100644 node/levin/compat.go delete mode 100644 node/levin/compat_test.go delete mode 100644 ueps/compat.go delete mode 100644 ueps/compat_test.go diff --git a/logging/compat.go b/logging/compat.go deleted file mode 100644 index 83a624d..0000000 --- a/logging/compat.go +++ /dev/null @@ -1,25 +0,0 @@ -package logging - -// WithComponent returns a new Logger scoped to one component. -// Deprecated: Use ComponentLogger instead. -// -// transportLogger := logger.WithComponent("transport") -func (l *Logger) WithComponent(component string) *Logger { - return l.ComponentLogger(component) -} - -// GetLevel returns the current log level. -// Preferred over Level for AX naming consistency. -// -// level := logger.GetLevel() -func (l *Logger) GetLevel() Level { - return l.Level() -} - -// GetGlobal returns the global logger instance. -// Deprecated: Use Global instead. -// -// logger := GetGlobal() -func GetGlobal() *Logger { - return Global() -} diff --git a/logging/compat_test.go b/logging/compat_test.go deleted file mode 100644 index 1310c17..0000000 --- a/logging/compat_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package logging - -import ( - "bytes" - "testing" - - core "dappco.re/go/core" -) - -func TestGetLevel_Good(t *testing.T) { - logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) - if logger.GetLevel() != LevelWarn { - t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) - } - if logger.GetLevel() != logger.Level() { - t.Error("GetLevel() and Level() should return the same value") - } -} - -func TestWithComponent_Good(t *testing.T) { - var buf bytes.Buffer - parent := New(Config{Output: &buf, Level: LevelInfo}) - child := parent.WithComponent("TestChild") - child.Info("test message") - if !core.Contains(buf.String(), "[TestChild]") { - t.Error("WithComponent should set the component name") - } -} - -func TestGetGlobal_Good(t *testing.T) { - var buf bytes.Buffer - logger := New(Config{Output: &buf, Level: LevelInfo}) - SetGlobal(logger) - got := GetGlobal() - if got != Global() { - t.Error("GetGlobal() and Global() should return the same logger") - } - SetGlobal(New(DefaultConfig())) -} diff --git a/node/compat.go b/node/compat.go deleted file mode 100644 index fce12ad..0000000 --- a/node/compat.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides deprecated compatibility aliases and type aliases -// for spec compatibility. Canonical names are defined in their respective -// source files; these aliases exist so that code written against the spec -// documentation continues to compile. - -package node - -// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. -// Deprecated: Use NewNodeManagerFromPaths instead. -// -// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - -// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. -// Deprecated: Use NewPeerRegistryFromPath instead. -// -// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - -// RegisterWithTransport installs the worker message handler on the transport. -// Deprecated: Use RegisterOnTransport instead. -// -// worker.RegisterWithTransport() -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} - -// ConnectedPeers returns the number of connected peers. -// Deprecated: Use ConnectedPeerCount instead. -// -// count := transport.ConnectedPeers() -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} - -// GetLogsPayload is an alias for LogsRequestPayload. -// Provided for spec compatibility. -// -// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} -type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go deleted file mode 100644 index 9b197aa..0000000 --- a/node/compat_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewNodeManagerWithPaths_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - - nm, err := NewNodeManagerWithPaths(keyPath, configPath) - require.NoError(t, err) - assert.NotNil(t, nm) - assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") -} - -func TestNewPeerRegistryWithPath_Good(t *testing.T) { - peersPath := t.TempDir() + "/peers.json" - - pr, err := NewPeerRegistryWithPath(peersPath) - require.NoError(t, err) - assert.NotNil(t, pr) - assert.Equal(t, 0, pr.Count()) - pr.Close() -} - -func TestGetLogsPayload_Good(t *testing.T) { - var payload GetLogsPayload - payload.MinerName = "xmrig-0" - payload.Lines = 100 - payload.Since = 1234567890 - - var request LogsRequestPayload = payload - assert.Equal(t, "xmrig-0", request.MinerName) - assert.Equal(t, 100, request.Lines) - assert.Equal(t, int64(1234567890), request.Since) -} - -func TestTransportConnectedPeers_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - peersPath := t.TempDir() + "/peers.json" - - nm, err := NewNodeManagerFromPaths(keyPath, configPath) - require.NoError(t, err) - - pr, err := NewPeerRegistryFromPath(peersPath) - require.NoError(t, err) - defer pr.Close() - - transport := NewTransport(nm, pr, DefaultTransportConfig()) - assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) - assert.Equal(t, 0, transport.ConnectedPeers()) -} diff --git a/node/levin/compat.go b/node/levin/compat.go deleted file mode 100644 index a88bdfb..0000000 --- a/node/levin/compat.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides short-form aliases for Value constructors, -// matching the naming used in the spec documentation. - -package levin - -// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. -// -// value := Uint64Val(42) -func Uint64Val(v uint64) Value { return Uint64Value(v) } - -// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. -// -// value := Uint32Val(42) -func Uint32Val(v uint32) Value { return Uint32Value(v) } - -// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. -// -// value := Uint16Val(42) -func Uint16Val(v uint16) Value { return Uint16Value(v) } - -// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. -// -// value := Uint8Val(42) -func Uint8Val(v uint8) Value { return Uint8Value(v) } - -// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. -// -// value := Int64Val(42) -func Int64Val(v int64) Value { return Int64Value(v) } - -// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. -// -// value := Int32Val(42) -func Int32Val(v int32) Value { return Int32Value(v) } - -// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. -// -// value := Int16Val(42) -func Int16Val(v int16) Value { return Int16Value(v) } - -// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. -// -// value := Int8Val(42) -func Int8Val(v int8) Value { return Int8Value(v) } - -// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. -// -// value := BoolVal(true) -func BoolVal(v bool) Value { return BoolValue(v) } - -// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. -// -// value := DoubleVal(3.14) -func DoubleVal(v float64) Value { return DoubleValue(v) } - -// StringVal creates a Value of TypeString. Short-form alias for StringValue. -// -// value := StringVal([]byte("hello")) -func StringVal(v []byte) Value { return StringValue(v) } - -// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. -// -// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) -func ObjectVal(s Section) Value { return ObjectValue(s) } - -// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. -// -// value := Uint64ArrayVal([]uint64{1, 2, 3}) -func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } - -// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. -// -// value := Uint32ArrayVal([]uint32{1, 2, 3}) -func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } - -// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. -// -// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) -func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } - -// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. -// -// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) -func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go deleted file mode 100644 index 72de5f4..0000000 --- a/node/levin/compat_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -package levin - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestShortFormAliases_Good(t *testing.T) { - assert.Equal(t, Uint64Value(42), Uint64Val(42)) - assert.Equal(t, Uint32Value(42), Uint32Val(42)) - assert.Equal(t, Uint16Value(42), Uint16Val(42)) - assert.Equal(t, Uint8Value(42), Uint8Val(42)) - assert.Equal(t, Int64Value(-42), Int64Val(-42)) - assert.Equal(t, Int32Value(-42), Int32Val(-42)) - assert.Equal(t, Int16Value(-42), Int16Val(-42)) - assert.Equal(t, Int8Value(-42), Int8Val(-42)) - assert.Equal(t, BoolValue(true), BoolVal(true)) - assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) - assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) - - section := Section{"key": StringValue([]byte("value"))} - assert.Equal(t, ObjectValue(section), ObjectVal(section)) - - assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) - assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) - assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) - assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) -} - -func TestShortFormAliasesRoundTrip_Good(t *testing.T) { - s := Section{ - "u64": Uint64Val(0xCAFEBABE), - "str": StringVal([]byte("hello")), - "flag": BoolVal(true), - "obj": ObjectVal(Section{"nested": Int32Val(42)}), - } - - data, err := EncodeStorage(s) - require.NoError(t, err) - - decoded, err := DecodeStorage(data) - require.NoError(t, err) - - u64, err := decoded["u64"].AsUint64() - require.NoError(t, err) - assert.Equal(t, uint64(0xCAFEBABE), u64) - - str, err := decoded["str"].AsString() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), str) - - flag, err := decoded["flag"].AsBool() - require.NoError(t, err) - assert.True(t, flag) - - obj, err := decoded["obj"].AsSection() - require.NoError(t, err) - nested, err := obj["nested"].AsInt32() - require.NoError(t, err) - assert.Equal(t, int32(42), nested) -} diff --git a/ueps/compat.go b/ueps/compat.go deleted file mode 100644 index 39d715a..0000000 --- a/ueps/compat.go +++ /dev/null @@ -1,9 +0,0 @@ -package ueps - -// Short-form tag aliases for spec compatibility. -const ( - // TagCurrentLay is a short-form alias for TagCurrentLayer. - TagCurrentLay = TagCurrentLayer - // TagTargetLay is a short-form alias for TagTargetLayer. - TagTargetLay = TagTargetLayer -) diff --git a/ueps/compat_test.go b/ueps/compat_test.go deleted file mode 100644 index d57fc01..0000000 --- a/ueps/compat_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package ueps - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTagAliases_Good(t *testing.T) { - assert.Equal(t, TagCurrentLayer, TagCurrentLay) - assert.Equal(t, TagTargetLayer, TagTargetLay) - assert.Equal(t, 0x02, TagCurrentLay) - assert.Equal(t, 0x03, TagTargetLay) -} -- 2.45.3 From cc0715785d8ab5011ea0a2c6cc879cd94272faa5 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:11:24 +0000 Subject: [PATCH 35/60] fix(node): restore AX compatibility aliases Co-Authored-By: Virgil --- logging/compat.go | 27 ++++++++++++ logging/compat_test.go | 39 ++++++++++++++++++ node/compat.go | 46 +++++++++++++++++++++ node/compat_test.go | 57 +++++++++++++++++++++++++ node/levin/compat.go | 87 +++++++++++++++++++++++++++++++++++++++ node/levin/compat_test.go | 66 +++++++++++++++++++++++++++++ ueps/compat.go | 11 +++++ ueps/compat_test.go | 14 +++++++ 8 files changed, 347 insertions(+) create mode 100644 logging/compat.go create mode 100644 logging/compat_test.go create mode 100644 node/compat.go create mode 100644 node/compat_test.go create mode 100644 node/levin/compat.go create mode 100644 node/levin/compat_test.go create mode 100644 ueps/compat.go create mode 100644 ueps/compat_test.go diff --git a/logging/compat.go b/logging/compat.go new file mode 100644 index 0000000..404910c --- /dev/null +++ b/logging/compat.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package logging + +// WithComponent returns a new Logger scoped to one component. +// Deprecated: Use ComponentLogger instead. +// +// transportLogger := logger.WithComponent("transport") +func (l *Logger) WithComponent(component string) *Logger { + return l.ComponentLogger(component) +} + +// GetLevel returns the current log level. +// Preferred over Level for AX naming consistency. +// +// level := logger.GetLevel() +func (l *Logger) GetLevel() Level { + return l.Level() +} + +// GetGlobal returns the global logger instance. +// Deprecated: Use Global instead. +// +// logger := GetGlobal() +func GetGlobal() *Logger { + return Global() +} diff --git a/logging/compat_test.go b/logging/compat_test.go new file mode 100644 index 0000000..1310c17 --- /dev/null +++ b/logging/compat_test.go @@ -0,0 +1,39 @@ +package logging + +import ( + "bytes" + "testing" + + core "dappco.re/go/core" +) + +func TestGetLevel_Good(t *testing.T) { + logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) + if logger.GetLevel() != LevelWarn { + t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) + } + if logger.GetLevel() != logger.Level() { + t.Error("GetLevel() and Level() should return the same value") + } +} + +func TestWithComponent_Good(t *testing.T) { + var buf bytes.Buffer + parent := New(Config{Output: &buf, Level: LevelInfo}) + child := parent.WithComponent("TestChild") + child.Info("test message") + if !core.Contains(buf.String(), "[TestChild]") { + t.Error("WithComponent should set the component name") + } +} + +func TestGetGlobal_Good(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{Output: &buf, Level: LevelInfo}) + SetGlobal(logger) + got := GetGlobal() + if got != Global() { + t.Error("GetGlobal() and Global() should return the same logger") + } + SetGlobal(New(DefaultConfig())) +} diff --git a/node/compat.go b/node/compat.go new file mode 100644 index 0000000..fce12ad --- /dev/null +++ b/node/compat.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// This file provides deprecated compatibility aliases and type aliases +// for spec compatibility. Canonical names are defined in their respective +// source files; these aliases exist so that code written against the spec +// documentation continues to compile. + +package node + +// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. +// Deprecated: Use NewNodeManagerFromPaths instead. +// +// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { + return NewNodeManagerFromPaths(keyPath, configPath) +} + +// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. +// Deprecated: Use NewPeerRegistryFromPath instead. +// +// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") +func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { + return NewPeerRegistryFromPath(peersPath) +} + +// RegisterWithTransport installs the worker message handler on the transport. +// Deprecated: Use RegisterOnTransport instead. +// +// worker.RegisterWithTransport() +func (w *Worker) RegisterWithTransport() { + w.RegisterOnTransport() +} + +// ConnectedPeers returns the number of connected peers. +// Deprecated: Use ConnectedPeerCount instead. +// +// count := transport.ConnectedPeers() +func (t *Transport) ConnectedPeers() int { + return t.ConnectedPeerCount() +} + +// GetLogsPayload is an alias for LogsRequestPayload. +// Provided for spec compatibility. +// +// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} +type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go new file mode 100644 index 0000000..9b197aa --- /dev/null +++ b/node/compat_test.go @@ -0,0 +1,57 @@ +package node + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewNodeManagerWithPaths_Good(t *testing.T) { + keyPath := t.TempDir() + "/private.key" + configPath := t.TempDir() + "/node.json" + + nm, err := NewNodeManagerWithPaths(keyPath, configPath) + require.NoError(t, err) + assert.NotNil(t, nm) + assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") +} + +func TestNewPeerRegistryWithPath_Good(t *testing.T) { + peersPath := t.TempDir() + "/peers.json" + + pr, err := NewPeerRegistryWithPath(peersPath) + require.NoError(t, err) + assert.NotNil(t, pr) + assert.Equal(t, 0, pr.Count()) + pr.Close() +} + +func TestGetLogsPayload_Good(t *testing.T) { + var payload GetLogsPayload + payload.MinerName = "xmrig-0" + payload.Lines = 100 + payload.Since = 1234567890 + + var request LogsRequestPayload = payload + assert.Equal(t, "xmrig-0", request.MinerName) + assert.Equal(t, 100, request.Lines) + assert.Equal(t, int64(1234567890), request.Since) +} + +func TestTransportConnectedPeers_Good(t *testing.T) { + keyPath := t.TempDir() + "/private.key" + configPath := t.TempDir() + "/node.json" + peersPath := t.TempDir() + "/peers.json" + + nm, err := NewNodeManagerFromPaths(keyPath, configPath) + require.NoError(t, err) + + pr, err := NewPeerRegistryFromPath(peersPath) + require.NoError(t, err) + defer pr.Close() + + transport := NewTransport(nm, pr, DefaultTransportConfig()) + assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) + assert.Equal(t, 0, transport.ConnectedPeers()) +} diff --git a/node/levin/compat.go b/node/levin/compat.go new file mode 100644 index 0000000..a88bdfb --- /dev/null +++ b/node/levin/compat.go @@ -0,0 +1,87 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +// This file provides short-form aliases for Value constructors, +// matching the naming used in the spec documentation. + +package levin + +// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. +// +// value := Uint64Val(42) +func Uint64Val(v uint64) Value { return Uint64Value(v) } + +// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. +// +// value := Uint32Val(42) +func Uint32Val(v uint32) Value { return Uint32Value(v) } + +// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. +// +// value := Uint16Val(42) +func Uint16Val(v uint16) Value { return Uint16Value(v) } + +// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. +// +// value := Uint8Val(42) +func Uint8Val(v uint8) Value { return Uint8Value(v) } + +// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. +// +// value := Int64Val(42) +func Int64Val(v int64) Value { return Int64Value(v) } + +// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. +// +// value := Int32Val(42) +func Int32Val(v int32) Value { return Int32Value(v) } + +// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. +// +// value := Int16Val(42) +func Int16Val(v int16) Value { return Int16Value(v) } + +// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. +// +// value := Int8Val(42) +func Int8Val(v int8) Value { return Int8Value(v) } + +// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. +// +// value := BoolVal(true) +func BoolVal(v bool) Value { return BoolValue(v) } + +// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. +// +// value := DoubleVal(3.14) +func DoubleVal(v float64) Value { return DoubleValue(v) } + +// StringVal creates a Value of TypeString. Short-form alias for StringValue. +// +// value := StringVal([]byte("hello")) +func StringVal(v []byte) Value { return StringValue(v) } + +// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. +// +// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) +func ObjectVal(s Section) Value { return ObjectValue(s) } + +// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. +// +// value := Uint64ArrayVal([]uint64{1, 2, 3}) +func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } + +// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. +// +// value := Uint32ArrayVal([]uint32{1, 2, 3}) +func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } + +// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. +// +// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) +func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } + +// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. +// +// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) +func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go new file mode 100644 index 0000000..72de5f4 --- /dev/null +++ b/node/levin/compat_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +package levin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShortFormAliases_Good(t *testing.T) { + assert.Equal(t, Uint64Value(42), Uint64Val(42)) + assert.Equal(t, Uint32Value(42), Uint32Val(42)) + assert.Equal(t, Uint16Value(42), Uint16Val(42)) + assert.Equal(t, Uint8Value(42), Uint8Val(42)) + assert.Equal(t, Int64Value(-42), Int64Val(-42)) + assert.Equal(t, Int32Value(-42), Int32Val(-42)) + assert.Equal(t, Int16Value(-42), Int16Val(-42)) + assert.Equal(t, Int8Value(-42), Int8Val(-42)) + assert.Equal(t, BoolValue(true), BoolVal(true)) + assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) + assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) + + section := Section{"key": StringValue([]byte("value"))} + assert.Equal(t, ObjectValue(section), ObjectVal(section)) + + assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) + assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) + assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) + assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) +} + +func TestShortFormAliasesRoundTrip_Good(t *testing.T) { + s := Section{ + "u64": Uint64Val(0xCAFEBABE), + "str": StringVal([]byte("hello")), + "flag": BoolVal(true), + "obj": ObjectVal(Section{"nested": Int32Val(42)}), + } + + data, err := EncodeStorage(s) + require.NoError(t, err) + + decoded, err := DecodeStorage(data) + require.NoError(t, err) + + u64, err := decoded["u64"].AsUint64() + require.NoError(t, err) + assert.Equal(t, uint64(0xCAFEBABE), u64) + + str, err := decoded["str"].AsString() + require.NoError(t, err) + assert.Equal(t, []byte("hello"), str) + + flag, err := decoded["flag"].AsBool() + require.NoError(t, err) + assert.True(t, flag) + + obj, err := decoded["obj"].AsSection() + require.NoError(t, err) + nested, err := obj["nested"].AsInt32() + require.NoError(t, err) + assert.Equal(t, int32(42), nested) +} diff --git a/ueps/compat.go b/ueps/compat.go new file mode 100644 index 0000000..2b81812 --- /dev/null +++ b/ueps/compat.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package ueps + +// Short-form tag aliases for spec compatibility. +const ( + // TagCurrentLay is a short-form alias for TagCurrentLayer. + TagCurrentLay = TagCurrentLayer + // TagTargetLay is a short-form alias for TagTargetLayer. + TagTargetLay = TagTargetLayer +) diff --git a/ueps/compat_test.go b/ueps/compat_test.go new file mode 100644 index 0000000..d57fc01 --- /dev/null +++ b/ueps/compat_test.go @@ -0,0 +1,14 @@ +package ueps + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTagAliases_Good(t *testing.T) { + assert.Equal(t, TagCurrentLayer, TagCurrentLay) + assert.Equal(t, TagTargetLayer, TagTargetLay) + assert.Equal(t, 0x02, TagCurrentLay) + assert.Equal(t, 0x03, TagTargetLay) +} -- 2.45.3 From c2e60c6ecef6a71c7aeef0b51f7c6722288be3ac Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:20:17 +0000 Subject: [PATCH 36/60] refactor(logging): prefer get-style logger accessors Co-Authored-By: Virgil --- logging/compat.go | 20 ++++++++++---------- logging/logger.go | 28 ++++++++++++++-------------- logging/logger_test.go | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/logging/compat.go b/logging/compat.go index 404910c..74f014b 100644 --- a/logging/compat.go +++ b/logging/compat.go @@ -10,18 +10,18 @@ func (l *Logger) WithComponent(component string) *Logger { return l.ComponentLogger(component) } -// GetLevel returns the current log level. -// Preferred over Level for AX naming consistency. +// Level returns the current log level. +// Deprecated: Use GetLevel instead. // -// level := logger.GetLevel() -func (l *Logger) GetLevel() Level { - return l.Level() +// level := logger.Level() +func (l *Logger) Level() Level { + return l.GetLevel() } -// GetGlobal returns the global logger instance. -// Deprecated: Use Global instead. +// Global returns the global logger instance. +// Deprecated: Use GetGlobal instead. // -// logger := GetGlobal() -func GetGlobal() *Logger { - return Global() +// logger := Global() +func Global() *Logger { + return GetGlobal() } diff --git a/logging/logger.go b/logging/logger.go index a24c227..2746881 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -107,10 +107,10 @@ func (l *Logger) SetLevel(level Level) { l.level = level } -// Level returns the current log level. +// GetLevel returns the current log level. // -// level := logger.Level() -func (l *Logger) Level() Level { +// level := logger.GetLevel() +func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level @@ -242,10 +242,10 @@ func SetGlobal(l *Logger) { globalLogger = l } -// Global returns the global logger instance. +// GetGlobal returns the global logger instance. // -// logger := Global() -func Global() *Logger { +// logger := GetGlobal() +func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger @@ -266,56 +266,56 @@ func SetGlobalLevel(level Level) { // // Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { - Global().Debug(message, fields...) + GetGlobal().Debug(message, fields...) } // Info logs an informational message using the global logger. // // Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { - Global().Info(message, fields...) + GetGlobal().Info(message, fields...) } // Warn logs a warning message using the global logger. // // Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { - Global().Warn(message, fields...) + GetGlobal().Warn(message, fields...) } // Error logs an error message using the global logger. // // Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { - Global().Error(message, fields...) + GetGlobal().Error(message, fields...) } // Debugf logs a formatted debug message using the global logger. // // Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { - Global().Debugf(format, args...) + GetGlobal().Debugf(format, args...) } // Infof logs a formatted informational message using the global logger. // // Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { - Global().Infof(format, args...) + GetGlobal().Infof(format, args...) } // Warnf logs a formatted warning message using the global logger. // // Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { - Global().Warnf(format, args...) + GetGlobal().Warnf(format, args...) } // Errorf logs a formatted error message using the global logger. // // Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { - Global().Errorf(format, args...) + GetGlobal().Errorf(format, args...) } // ParseLevel parses a string into a log level. diff --git a/logging/logger_test.go b/logging/logger_test.go index 08c48a9..b46aaec 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -149,8 +149,8 @@ func TestLogger_SetLevel_Good(t *testing.T) { } // Verify Level - if logger.Level() != LevelInfo { - t.Error("Level should return LevelInfo") + if logger.GetLevel() != LevelInfo { + t.Error("GetLevel should return LevelInfo") } } -- 2.45.3 From 711a43aa3f91a068b5917219973fd2812776fd70 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:26:00 +0000 Subject: [PATCH 37/60] refactor(node): remove AX compatibility aliases Co-Authored-By: Virgil --- docs/architecture.md | 8 ++-- docs/development.md | 2 +- docs/history.md | 4 +- docs/transport.md | 12 +++--- docs/ueps.md | 4 +- logging/compat.go | 27 ------------ logging/compat_test.go | 39 ------------------ node/compat.go | 46 --------------------- node/compat_test.go | 57 ------------------------- node/levin/compat.go | 87 --------------------------------------- node/levin/compat_test.go | 66 ----------------------------- specs/logging.md | 5 +-- specs/node-levin.md | 34 +++++++-------- specs/node.md | 11 ++--- ueps/compat.go | 11 ----- ueps/compat_test.go | 14 ------- 16 files changed, 38 insertions(+), 389 deletions(-) delete mode 100644 logging/compat.go delete mode 100644 logging/compat_test.go delete mode 100644 node/compat.go delete mode 100644 node/compat_test.go delete mode 100644 node/levin/compat.go delete mode 100644 node/levin/compat_test.go delete mode 100644 ueps/compat.go delete mode 100644 ueps/compat_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 105c0ec..1f9f3a2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,8 +37,8 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| | `ListenAddr` | `:9091` | HTTP bind address | -| `WSPath` | `/ws` | WebSocket endpoint | -| `MaxConns` | 100 | Maximum concurrent connections | +| `WebSocketPath` | `/ws` | WebSocket endpoint | +| `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | | `PingInterval` | 30 s | Keepalive ping period | | `PongTimeout` | 10 s | Maximum time to wait for pong | @@ -56,7 +56,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Rate limiting**: Each `PeerConnection` holds a `PeerRateLimiter` (token bucket: 100 burst, 50 tokens/second refill). Messages from rate-limited peers are dropped in the read loop. -**MaxConns enforcement**: The handler tracks `pendingConns` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. +**MaxConnections enforcement**: The handler tracks `pendingHandshakeCount` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. @@ -244,7 +244,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. | `Controller.pending` | `sync.RWMutex` | | `MessageDeduplicator.seen` | `sync.RWMutex` | | `Dispatcher.handlers` | `sync.RWMutex` | -| `Transport.pendingConns` | `atomic.Int32` | +| `Transport.pendingHandshakeCount` | `atomic.Int32` | The codebase is verified race-free under `go test -race`. diff --git a/docs/development.md b/docs/development.md index 8d2bd3b..196c370 100644 --- a/docs/development.md +++ b/docs/development.md @@ -233,7 +233,7 @@ Examples: ``` feat(dispatcher): implement UEPS threat circuit breaker -test(transport): add keepalive timeout and MaxConns enforcement tests +test(transport): add keepalive timeout and MaxConnections enforcement tests fix(peer): prevent data race in GracefulClose (P2P-RACE-1) ``` diff --git a/docs/history.md b/docs/history.md index 52ea3f2..5c42f56 100644 --- a/docs/history.md +++ b/docs/history.md @@ -28,7 +28,7 @@ Tests covered: - Encrypted message round-trip: SMSG encrypt on one side, decrypt on other - Message deduplication: duplicate UUID dropped silently - Rate limiting: burst of more than 100 messages, subsequent drops after token bucket empties -- MaxConns enforcement: 503 HTTP rejection when limit is reached +- MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close - Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) @@ -92,7 +92,7 @@ The `TagPayload` (0xFF) field now uses the same 2-byte length prefix as the othe ### No Resource Cleanup on Some Error Paths -`transport.handleWSUpgrade` does not clean up on handshake timeout (the `pendingConns` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). +`transport.handleWebSocketUpgrade` does not clean up on handshake timeout (the `pendingHandshakeCount` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). These are low-severity gaps. They do not cause goroutine leaks under the current implementation because the connection's read loop is not started until after a successful handshake. diff --git a/docs/transport.md b/docs/transport.md index 19a7987..5aad542 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -12,10 +12,10 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { ListenAddr string // ":9091" default - WSPath string // "/ws" -- WebSocket endpoint path + WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string - MaxConns int // Maximum concurrent connections (default 100) + MaxConnections int // Maximum concurrent connections (default 100) MaxMessageSize int64 // Maximum message size in bytes (default 1MB) PingInterval time.Duration // Keepalive interval (default 30s) PongTimeout time.Duration // Pong wait timeout (default 10s) @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go cfg := node.DefaultTransportConfig() -// ListenAddr: ":9091", WSPath: "/ws", MaxConns: 100 +// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -123,9 +123,9 @@ const ( ## Incoming Connections -The transport exposes an HTTP handler at the configured `WSPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. +The transport exposes an HTTP handler at the configured `WebSocketPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. -The `MaxConns` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. +The `MaxConnections` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. ## Message Deduplication @@ -166,7 +166,7 @@ err = transport.Send(peerID, msg) err = transport.Broadcast(msg) // Query connections -count := transport.ConnectedPeers() +count := transport.ConnectedPeerCount() conn := transport.GetConnection(peerID) // Iterate over all connections diff --git a/docs/ueps.md b/docs/ueps.md index 2ba184a..09c0b71 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -25,8 +25,8 @@ Each field is encoded as a 1-byte tag, 2-byte big-endian length (uint16), and va | Tag | Constant | Value Size | Description | |-----|----------|------------|-------------| | `0x01` | `TagVersion` | 1 byte | Protocol version (default `0x09` for IPv9) | -| `0x02` | `TagCurrentLay` | 1 byte | Current network layer | -| `0x03` | `TagTargetLay` | 1 byte | Target network layer | +| `0x02` | `TagCurrentLayer` | 1 byte | Current network layer | +| `0x03` | `TagTargetLayer` | 1 byte | Target network layer | | `0x04` | `TagIntent` | 1 byte | Semantic intent token (routes the packet) | | `0x05` | `TagThreatScore` | 2 bytes | Threat score (0--65535, big-endian uint16) | | `0x06` | `TagHMAC` | 32 bytes | HMAC-SHA256 signature | diff --git a/logging/compat.go b/logging/compat.go deleted file mode 100644 index 74f014b..0000000 --- a/logging/compat.go +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package logging - -// WithComponent returns a new Logger scoped to one component. -// Deprecated: Use ComponentLogger instead. -// -// transportLogger := logger.WithComponent("transport") -func (l *Logger) WithComponent(component string) *Logger { - return l.ComponentLogger(component) -} - -// Level returns the current log level. -// Deprecated: Use GetLevel instead. -// -// level := logger.Level() -func (l *Logger) Level() Level { - return l.GetLevel() -} - -// Global returns the global logger instance. -// Deprecated: Use GetGlobal instead. -// -// logger := Global() -func Global() *Logger { - return GetGlobal() -} diff --git a/logging/compat_test.go b/logging/compat_test.go deleted file mode 100644 index 1310c17..0000000 --- a/logging/compat_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package logging - -import ( - "bytes" - "testing" - - core "dappco.re/go/core" -) - -func TestGetLevel_Good(t *testing.T) { - logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) - if logger.GetLevel() != LevelWarn { - t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) - } - if logger.GetLevel() != logger.Level() { - t.Error("GetLevel() and Level() should return the same value") - } -} - -func TestWithComponent_Good(t *testing.T) { - var buf bytes.Buffer - parent := New(Config{Output: &buf, Level: LevelInfo}) - child := parent.WithComponent("TestChild") - child.Info("test message") - if !core.Contains(buf.String(), "[TestChild]") { - t.Error("WithComponent should set the component name") - } -} - -func TestGetGlobal_Good(t *testing.T) { - var buf bytes.Buffer - logger := New(Config{Output: &buf, Level: LevelInfo}) - SetGlobal(logger) - got := GetGlobal() - if got != Global() { - t.Error("GetGlobal() and Global() should return the same logger") - } - SetGlobal(New(DefaultConfig())) -} diff --git a/node/compat.go b/node/compat.go deleted file mode 100644 index fce12ad..0000000 --- a/node/compat.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides deprecated compatibility aliases and type aliases -// for spec compatibility. Canonical names are defined in their respective -// source files; these aliases exist so that code written against the spec -// documentation continues to compile. - -package node - -// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. -// Deprecated: Use NewNodeManagerFromPaths instead. -// -// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - -// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. -// Deprecated: Use NewPeerRegistryFromPath instead. -// -// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - -// RegisterWithTransport installs the worker message handler on the transport. -// Deprecated: Use RegisterOnTransport instead. -// -// worker.RegisterWithTransport() -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} - -// ConnectedPeers returns the number of connected peers. -// Deprecated: Use ConnectedPeerCount instead. -// -// count := transport.ConnectedPeers() -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} - -// GetLogsPayload is an alias for LogsRequestPayload. -// Provided for spec compatibility. -// -// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} -type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go deleted file mode 100644 index 9b197aa..0000000 --- a/node/compat_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewNodeManagerWithPaths_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - - nm, err := NewNodeManagerWithPaths(keyPath, configPath) - require.NoError(t, err) - assert.NotNil(t, nm) - assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") -} - -func TestNewPeerRegistryWithPath_Good(t *testing.T) { - peersPath := t.TempDir() + "/peers.json" - - pr, err := NewPeerRegistryWithPath(peersPath) - require.NoError(t, err) - assert.NotNil(t, pr) - assert.Equal(t, 0, pr.Count()) - pr.Close() -} - -func TestGetLogsPayload_Good(t *testing.T) { - var payload GetLogsPayload - payload.MinerName = "xmrig-0" - payload.Lines = 100 - payload.Since = 1234567890 - - var request LogsRequestPayload = payload - assert.Equal(t, "xmrig-0", request.MinerName) - assert.Equal(t, 100, request.Lines) - assert.Equal(t, int64(1234567890), request.Since) -} - -func TestTransportConnectedPeers_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - peersPath := t.TempDir() + "/peers.json" - - nm, err := NewNodeManagerFromPaths(keyPath, configPath) - require.NoError(t, err) - - pr, err := NewPeerRegistryFromPath(peersPath) - require.NoError(t, err) - defer pr.Close() - - transport := NewTransport(nm, pr, DefaultTransportConfig()) - assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) - assert.Equal(t, 0, transport.ConnectedPeers()) -} diff --git a/node/levin/compat.go b/node/levin/compat.go deleted file mode 100644 index a88bdfb..0000000 --- a/node/levin/compat.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides short-form aliases for Value constructors, -// matching the naming used in the spec documentation. - -package levin - -// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. -// -// value := Uint64Val(42) -func Uint64Val(v uint64) Value { return Uint64Value(v) } - -// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. -// -// value := Uint32Val(42) -func Uint32Val(v uint32) Value { return Uint32Value(v) } - -// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. -// -// value := Uint16Val(42) -func Uint16Val(v uint16) Value { return Uint16Value(v) } - -// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. -// -// value := Uint8Val(42) -func Uint8Val(v uint8) Value { return Uint8Value(v) } - -// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. -// -// value := Int64Val(42) -func Int64Val(v int64) Value { return Int64Value(v) } - -// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. -// -// value := Int32Val(42) -func Int32Val(v int32) Value { return Int32Value(v) } - -// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. -// -// value := Int16Val(42) -func Int16Val(v int16) Value { return Int16Value(v) } - -// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. -// -// value := Int8Val(42) -func Int8Val(v int8) Value { return Int8Value(v) } - -// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. -// -// value := BoolVal(true) -func BoolVal(v bool) Value { return BoolValue(v) } - -// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. -// -// value := DoubleVal(3.14) -func DoubleVal(v float64) Value { return DoubleValue(v) } - -// StringVal creates a Value of TypeString. Short-form alias for StringValue. -// -// value := StringVal([]byte("hello")) -func StringVal(v []byte) Value { return StringValue(v) } - -// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. -// -// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) -func ObjectVal(s Section) Value { return ObjectValue(s) } - -// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. -// -// value := Uint64ArrayVal([]uint64{1, 2, 3}) -func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } - -// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. -// -// value := Uint32ArrayVal([]uint32{1, 2, 3}) -func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } - -// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. -// -// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) -func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } - -// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. -// -// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) -func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go deleted file mode 100644 index 72de5f4..0000000 --- a/node/levin/compat_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -package levin - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestShortFormAliases_Good(t *testing.T) { - assert.Equal(t, Uint64Value(42), Uint64Val(42)) - assert.Equal(t, Uint32Value(42), Uint32Val(42)) - assert.Equal(t, Uint16Value(42), Uint16Val(42)) - assert.Equal(t, Uint8Value(42), Uint8Val(42)) - assert.Equal(t, Int64Value(-42), Int64Val(-42)) - assert.Equal(t, Int32Value(-42), Int32Val(-42)) - assert.Equal(t, Int16Value(-42), Int16Val(-42)) - assert.Equal(t, Int8Value(-42), Int8Val(-42)) - assert.Equal(t, BoolValue(true), BoolVal(true)) - assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) - assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) - - section := Section{"key": StringValue([]byte("value"))} - assert.Equal(t, ObjectValue(section), ObjectVal(section)) - - assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) - assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) - assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) - assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) -} - -func TestShortFormAliasesRoundTrip_Good(t *testing.T) { - s := Section{ - "u64": Uint64Val(0xCAFEBABE), - "str": StringVal([]byte("hello")), - "flag": BoolVal(true), - "obj": ObjectVal(Section{"nested": Int32Val(42)}), - } - - data, err := EncodeStorage(s) - require.NoError(t, err) - - decoded, err := DecodeStorage(data) - require.NoError(t, err) - - u64, err := decoded["u64"].AsUint64() - require.NoError(t, err) - assert.Equal(t, uint64(0xCAFEBABE), u64) - - str, err := decoded["str"].AsString() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), str) - - flag, err := decoded["flag"].AsBool() - require.NoError(t, err) - assert.True(t, flag) - - obj, err := decoded["obj"].AsSection() - require.NoError(t, err) - nested, err := obj["nested"].AsInt32() - require.NoError(t, err) - assert.Equal(t, int32(42), nested) -} diff --git a/specs/logging.md b/specs/logging.md index 31de890..b3ba1ef 100644 --- a/specs/logging.md +++ b/specs/logging.md @@ -43,7 +43,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | | `DefaultConfig` | `func DefaultConfig() Config` | Returns the default configuration: stderr output, `LevelInfo`, and no component label. | -| `New` | `func New(cfg Config) *Logger` | Creates a `Logger` from `cfg`, substituting the default stderr writer when `cfg.Output` is `nil`. | +| `New` | `func New(config Config) *Logger` | Creates a `Logger` from `config`, substituting the default stderr writer when `config.Output` is `nil`. | | `SetGlobal` | `func SetGlobal(l *Logger)` | Replaces the package-level global logger instance. | | `GetGlobal` | `func GetGlobal() *Logger` | Returns the current package-level global logger. | | `SetGlobalLevel` | `func SetGlobalLevel(level Level)` | Updates the minimum severity on the current global logger. | @@ -67,8 +67,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | -| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. Preferred over `WithComponent`. | -| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Deprecated compatibility alias for `ComponentLogger`. | +| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. | | `SetLevel` | `func (l *Logger) SetLevel(level Level)` | Sets the minimum severity that the logger will emit. | | `GetLevel` | `func (l *Logger) GetLevel() Level` | Returns the current minimum severity. | | `Debug` | `func (l *Logger) Debug(msg string, fields ...Fields)` | Logs `msg` at debug level after merging any supplied field maps. | diff --git a/specs/node-levin.md b/specs/node-levin.md index b5e8912..ca6b93c 100644 --- a/specs/node-levin.md +++ b/specs/node-levin.md @@ -48,7 +48,7 @@ type Value struct { } ``` -Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Val`, `StringVal`, and `ObjectArrayVal` create correctly-typed instances. +Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Value`, `StringValue`, and `ObjectArrayValue` create correctly-typed instances. ## Functions @@ -68,22 +68,22 @@ Tagged portable-storage value. The exported `Type` field identifies which intern | Name | Signature | Description | | --- | --- | --- | -| `Uint64Val` | `func Uint64Val(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | -| `Uint32Val` | `func Uint32Val(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | -| `Uint16Val` | `func Uint16Val(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | -| `Uint8Val` | `func Uint8Val(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | -| `Int64Val` | `func Int64Val(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | -| `Int32Val` | `func Int32Val(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | -| `Int16Val` | `func Int16Val(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | -| `Int8Val` | `func Int8Val(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | -| `BoolVal` | `func BoolVal(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | -| `DoubleVal` | `func DoubleVal(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | -| `StringVal` | `func StringVal(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | -| `ObjectVal` | `func ObjectVal(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | -| `Uint64ArrayVal` | `func Uint64ArrayVal(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | -| `Uint32ArrayVal` | `func Uint32ArrayVal(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | -| `StringArrayVal` | `func StringArrayVal(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | -| `ObjectArrayVal` | `func ObjectArrayVal(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | +| `Uint64Value` | `func Uint64Value(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | +| `Uint32Value` | `func Uint32Value(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | +| `Uint16Value` | `func Uint16Value(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | +| `Uint8Value` | `func Uint8Value(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | +| `Int64Value` | `func Int64Value(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | +| `Int32Value` | `func Int32Value(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | +| `Int16Value` | `func Int16Value(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | +| `Int8Value` | `func Int8Value(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | +| `BoolValue` | `func BoolValue(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | +| `DoubleValue` | `func DoubleValue(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | +| `StringValue` | `func StringValue(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | +| `ObjectValue` | `func ObjectValue(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | +| `Uint64ArrayValue` | `func Uint64ArrayValue(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | +| `Uint32ArrayValue` | `func Uint32ArrayValue(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | +| `StringArrayValue` | `func StringArrayValue(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | +| `ObjectArrayValue` | `func ObjectArrayValue(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | ### `*Connection` methods diff --git a/specs/node.md b/specs/node.md index 87324b3..99f6eb8 100644 --- a/specs/node.md +++ b/specs/node.md @@ -32,7 +32,7 @@ | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WSPath string; TLSCertPath string; TLSKeyPath string; MaxConns int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -43,7 +43,7 @@ | `DeployPayload` | `struct{ BundleType string; Data []byte; Checksum string; Name string }` | Deployment request carrying STIM-encrypted bundle bytes (or other bundle data), checksum, and logical name. | | `DisconnectPayload` | `struct{ Reason string; Code int }` | Disconnect notice with human-readable reason and optional disconnect code. | | `ErrorPayload` | `struct{ Code int; Message string; Details string }` | Payload used by `MsgError` responses. | -| `GetLogsPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | +| `LogsRequestPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | | `HandshakeAckPayload` | `struct{ Identity NodeIdentity; ChallengeResponse []byte; Accepted bool; Reason string }` | Handshake reply containing the responder identity, optional challenge response, acceptance flag, and optional rejection reason. | | `HandshakePayload` | `struct{ Identity NodeIdentity; Challenge []byte; Version string }` | Handshake request containing node identity, optional authentication challenge, and protocol version. | | `LogsPayload` | `struct{ MinerName string; Lines []string; HasMore bool }` | Returned miner log lines plus an indicator that more lines are available. | @@ -88,17 +88,15 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `:9091`, `/ws`, `MaxConns=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | | `NewNodeManager` | `func NewNodeManager() (*NodeManager, error)` | Resolves XDG key and config paths, then loads an existing identity if present. | | `NewNodeManagerFromPaths` | `func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager from explicit key and config paths. | -| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Deprecated compatibility alias for `NewNodeManagerFromPaths`. | | `NewPeerRateLimiter` | `func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter` | Creates a token bucket seeded with `maxTokens` and refilled at `refillRate` tokens per second. | | `NewPeerRegistry` | `func NewPeerRegistry() (*PeerRegistry, error)` | Resolves the XDG peers path, loads any persisted peers, and builds the selection KD-tree. | | `NewPeerRegistryFromPath` | `func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | -| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Deprecated compatibility alias for `NewPeerRegistryFromPath`. | | `NewTransport` | `func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport` | Creates a transport with lifecycle context, a 5-minute message deduplicator, and a WebSocket upgrader that only accepts local origins. | | `NewWorker` | `func NewWorker(node *NodeManager, transport *Transport) *Worker` | Creates a worker, records its start time for uptime reporting, and defaults `DataDir` to `xdg.DataHome`. | @@ -213,7 +211,7 @@ | `Connections` | `func (t *Transport) Connections() iter.Seq[*PeerConnection]` | Returns an iterator over active peer connections. | | `Broadcast` | `func (t *Transport) Broadcast(msg *Message) error` | Sends `msg` to every connected peer except the sender identified by `msg.From`. | | `GetConnection` | `func (t *Transport) GetConnection(peerID string) *PeerConnection` | Returns the active connection for `peerID`, or `nil` when not connected. | -| `ConnectedPeers` | `func (t *Transport) ConnectedPeers() int` | Returns the number of active peer connections. | +| `ConnectedPeerCount` | `func (t *Transport) ConnectedPeerCount() int` | Returns the number of active peer connections. | ### `*PeerConnection` methods @@ -231,7 +229,6 @@ | `SetProfileManager` | `func (w *Worker) SetProfileManager(manager ProfileManager)` | Installs the profile manager used during deployment handling. | | `HandleMessage` | `func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message)` | Dispatches supported message types, sends normal replies on success, and emits `MsgError` responses when a handled command fails. | | `RegisterOnTransport` | `func (w *Worker) RegisterOnTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | -| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Deprecated compatibility alias for `RegisterOnTransport`. | ### `*ProtocolError` methods diff --git a/ueps/compat.go b/ueps/compat.go deleted file mode 100644 index 2b81812..0000000 --- a/ueps/compat.go +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package ueps - -// Short-form tag aliases for spec compatibility. -const ( - // TagCurrentLay is a short-form alias for TagCurrentLayer. - TagCurrentLay = TagCurrentLayer - // TagTargetLay is a short-form alias for TagTargetLayer. - TagTargetLay = TagTargetLayer -) diff --git a/ueps/compat_test.go b/ueps/compat_test.go deleted file mode 100644 index d57fc01..0000000 --- a/ueps/compat_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package ueps - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTagAliases_Good(t *testing.T) { - assert.Equal(t, TagCurrentLayer, TagCurrentLay) - assert.Equal(t, TagTargetLayer, TagTargetLay) - assert.Equal(t, 0x02, TagCurrentLay) - assert.Equal(t, 0x03, TagTargetLay) -} -- 2.45.3 From b5c7516224c4567f24114f1fb4d4fecfebb0ada3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:29:51 +0000 Subject: [PATCH 38/60] refactor(logging): remove stale alias wording Co-Authored-By: Virgil --- logging/logger.go | 9 +-------- logging/logger_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index 2746881..fed3fa9 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -27,7 +27,7 @@ const ( LevelError ) -// String returns the string representation of the log level. +// label := LevelWarn.String() func (l Level) String() string { switch l { case LevelDebug: @@ -133,7 +133,6 @@ func (stderrWriter) Write(p []byte) (int, error) { var defaultOutput io.Writer = stderrWriter{} -// log writes a log message at the specified level. func (l *Logger) log(level Level, message string, fields Fields) { l.mu.Lock() defer l.mu.Unlock() @@ -142,7 +141,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { return } - // Build the log line sb := core.NewBuilder() timestamp := time.Now().Format("2006/01/02 15:04:05") sb.WriteString(timestamp) @@ -159,7 +157,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { sb.WriteString(" ") sb.WriteString(message) - // Add fields if present if len(fields) > 0 { sb.WriteString(" |") for k, v := range fields { @@ -226,8 +223,6 @@ func mergeFields(fields []Fields) Fields { return result } -// --- Global logger for convenience --- - var ( globalLogger = New(DefaultConfig()) globalMu sync.RWMutex @@ -260,8 +255,6 @@ func SetGlobalLevel(level Level) { globalLogger.SetLevel(level) } -// Global convenience functions that use the global logger - // Debug logs a debug message using the global logger. // // Debug("connected", Fields{"peer_id": "node-1"}) diff --git a/logging/logger_test.go b/logging/logger_test.go index b46aaec..c553130 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -101,15 +101,15 @@ func TestLogger_ComponentLogger_Good(t *testing.T) { child := parent.ComponentLogger("ChildComponent") child.Info("child message") - alias := parent.ComponentLogger("AliasComponent") - alias.Info("alias message") + secondaryLogger := parent.ComponentLogger("SecondaryComponent") + secondaryLogger.Info("secondary message") output := buf.String() if !core.Contains(output, "[ChildComponent]") { t.Error("Derived component name should appear") } - if !core.Contains(output, "[AliasComponent]") { - t.Error("Compatibility alias should preserve the component name") + if !core.Contains(output, "[SecondaryComponent]") { + t.Error("Secondary component should preserve the component name") } } -- 2.45.3 From 8fc3be03a65036efb904759d5739beae68204404 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:36:45 +0000 Subject: [PATCH 39/60] refactor(node): align AX comments across public APIs Co-Authored-By: Virgil --- logging/logger.go | 84 ++++++++++------------------------------ node/controller.go | 4 +- node/dispatcher.go | 70 +++++++-------------------------- node/errors.go | 8 +--- node/identity.go | 6 +-- node/levin/connection.go | 36 +++++------------ node/message.go | 14 ++----- node/peer.go | 6 +-- node/protocol.go | 2 +- node/transport.go | 72 +++++++++------------------------- node/worker.go | 33 ++++------------ 11 files changed, 84 insertions(+), 251 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index fed3fa9..bbaac12 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -11,9 +11,7 @@ import ( core "dappco.re/go/core" ) -// Level represents the severity of a log message. -// -// level := LevelInfo +// level := LevelInfo type Level int const ( @@ -43,9 +41,7 @@ func (l Level) String() string { } } -// Logger provides structured logging with configurable output and level. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) type Logger struct { mu sync.RWMutex output io.Writer @@ -53,18 +49,14 @@ type Logger struct { component string } -// Config holds configuration for creating a new Logger. -// -// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} +// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} type Config struct { Output io.Writer Level Level Component string } -// DefaultConfig returns the default logger configuration. -// -// config := DefaultConfig() +// config := DefaultConfig() func DefaultConfig() Config { return Config{ Output: defaultOutput, @@ -73,9 +65,7 @@ func DefaultConfig() Config { } } -// New creates a logger from an explicit configuration. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) func New(config Config) *Logger { if config.Output == nil { config.Output = defaultOutput @@ -87,9 +77,7 @@ func New(config Config) *Logger { } } -// ComponentLogger returns a new Logger scoped to one component. -// -// transportLogger := logger.ComponentLogger("transport") +// transportLogger := logger.ComponentLogger("transport") func (l *Logger) ComponentLogger(component string) *Logger { return &Logger{ output: l.output, @@ -98,27 +86,21 @@ func (l *Logger) ComponentLogger(component string) *Logger { } } -// SetLevel changes the minimum log level. -// -// logger.SetLevel(LevelDebug) +// logger.SetLevel(LevelDebug) func (l *Logger) SetLevel(level Level) { l.mu.Lock() defer l.mu.Unlock() l.level = level } -// GetLevel returns the current log level. -// -// level := logger.GetLevel() +// level := logger.GetLevel() func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level } -// Fields represents key-value pairs for structured logging. -// -// fields := Fields{"peer_id": "node-1", "attempt": 2} +// fields := Fields{"peer_id": "node-1", "attempt": 2} type Fields map[string]any type stderrWriter struct{} @@ -228,92 +210,68 @@ var ( globalMu sync.RWMutex ) -// SetGlobal installs the global logger instance. -// -// SetGlobal(New(DefaultConfig())) +// SetGlobal(New(DefaultConfig())) func SetGlobal(l *Logger) { globalMu.Lock() defer globalMu.Unlock() globalLogger = l } -// GetGlobal returns the global logger instance. -// -// logger := GetGlobal() +// logger := GetGlobal() func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger } -// SetGlobalLevel changes the global logger level. -// -// SetGlobalLevel(LevelDebug) +// SetGlobalLevel(LevelDebug) func SetGlobalLevel(level Level) { globalMu.RLock() defer globalMu.RUnlock() globalLogger.SetLevel(level) } -// Debug logs a debug message using the global logger. -// -// Debug("connected", Fields{"peer_id": "node-1"}) +// Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { GetGlobal().Debug(message, fields...) } -// Info logs an informational message using the global logger. -// -// Info("worker started", Fields{"component": "transport"}) +// Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { GetGlobal().Info(message, fields...) } -// Warn logs a warning message using the global logger. -// -// Warn("peer rate limited", Fields{"peer_id": "node-1"}) +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { GetGlobal().Warn(message, fields...) } -// Error logs an error message using the global logger. -// -// Error("send failed", Fields{"peer_id": "node-1"}) +// Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { GetGlobal().Error(message, fields...) } -// Debugf logs a formatted debug message using the global logger. -// -// Debugf("connected peer %s", "node-1") +// Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { GetGlobal().Debugf(format, args...) } -// Infof logs a formatted informational message using the global logger. -// -// Infof("worker %s ready", "node-1") +// Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { GetGlobal().Infof(format, args...) } -// Warnf logs a formatted warning message using the global logger. -// -// Warnf("peer %s is slow", "node-1") +// Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { GetGlobal().Warnf(format, args...) } -// Errorf logs a formatted error message using the global logger. -// -// Errorf("peer %s failed", "node-1") +// Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { GetGlobal().Errorf(format, args...) } -// ParseLevel parses a string into a log level. -// -// level, err := ParseLevel("warn") +// level, err := ParseLevel("warn") func ParseLevel(s string) (Level, error) { switch core.Upper(s) { case "DEBUG": diff --git a/node/controller.go b/node/controller.go index a683738..3d4ebc9 100644 --- a/node/controller.go +++ b/node/controller.go @@ -10,9 +10,7 @@ import ( "dappco.re/go/core/p2p/logging" ) -// Controller drives remote peer operations from a controller node. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { nodeManager *NodeManager peerRegistry *PeerRegistry diff --git a/node/dispatcher.go b/node/dispatcher.go index 126e3b2..1307713 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -10,11 +10,7 @@ import ( "dappco.re/go/core/p2p/ueps" ) -// ThreatScoreThreshold is the maximum allowable threat score. Packets exceeding -// this value are silently dropped by the circuit breaker and logged as threat -// events. The threshold sits at ~76% of the uint16 range (50,000 / 65,535), -// providing headroom for legitimate elevated-risk traffic whilst rejecting -// clearly hostile payloads. +// threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 // Well-known intent identifiers. These correspond to the semantic tokens @@ -32,34 +28,16 @@ const ( // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error -// Dispatcher routes verified UEPS packets to registered intent handlers. -// It enforces a threat circuit breaker before routing: any packet whose -// ThreatScore exceeds ThreatScoreThreshold is dropped and logged. -// -// dispatcher := NewDispatcher() -// -// Design decisions: -// -// - Handlers are registered per IntentID (1:1 mapping). -// -// - Unknown intents are logged at WARN level and silently dropped (no error -// returned to the caller) to avoid back-pressure on the transport layer. -// -// - High-threat packets are dropped silently (logged at WARN) rather than -// returning an error, consistent with the "don't even parse the payload" -// philosophy from the original stub. -// -// - The dispatcher is safe for concurrent use; a RWMutex protects the -// handler map. +// dispatcher := NewDispatcher() +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) +// err := dispatcher.Dispatch(packet) type Dispatcher struct { handlers map[byte]IntentHandler mu sync.RWMutex log *logging.Logger } -// NewDispatcher creates a Dispatcher with no registered handlers. -// -// dispatcher := NewDispatcher() +// dispatcher := NewDispatcher() func NewDispatcher() *Dispatcher { return &Dispatcher{ handlers: make(map[byte]IntentHandler), @@ -70,12 +48,7 @@ func NewDispatcher() *Dispatcher { } } -// RegisterHandler associates an IntentHandler with a specific IntentID. -// Replacing an existing handler is allowed — the new handler takes effect immediately. -// -// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { -// return processComputeJob(packet.Payload) -// }) +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { d.mu.Lock() defer d.mu.Unlock() @@ -85,11 +58,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// Handlers returns an iterator over all registered intent handlers. -// -// for intentID, handler := range dispatcher.Handlers() { -// log.Printf("registered intent 0x%02X", intentID) -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() @@ -103,17 +75,7 @@ func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { } } -// Dispatch routes a parsed UEPS packet through the threat circuit breaker -// and then to the appropriate intent handler. -// -// Behaviour: -// - Returns ErrorThreatScoreExceeded if the packet's ThreatScore exceeds the -// threshold (packet is dropped and logged). -// - Returns ErrorUnknownIntent if no handler is registered for the IntentID -// (packet is dropped and logged). -// - Returns nil on successful delivery to a handler, or any error the -// handler itself returns. -// - A nil packet returns ErrorNilPacket immediately. +// err := dispatcher.Dispatch(packet) func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { if packet == nil { return ErrorNilPacket @@ -146,17 +108,13 @@ func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { return handler(packet) } -// Sentinel errors returned by Dispatch. var ( - // ErrorThreatScoreExceeded is returned when a packet's ThreatScore exceeds - // the safety threshold. + // err := ErrorThreatScoreExceeded ErrorThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) - // ErrorUnknownIntent is returned when no handler is registered for the - // packet's IntentID. + // err := ErrorUnknownIntent ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - // ErrorNilPacket is returned when a nil packet is passed to Dispatch. + // err := ErrorNilPacket ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) - diff --git a/node/errors.go b/node/errors.go index f660be1..302a34a 100644 --- a/node/errors.go +++ b/node/errors.go @@ -2,14 +2,10 @@ package node import core "dappco.re/go/core" -// Shared error sentinels for the node package. var ( - // ErrorIdentityNotInitialized is returned when a node operation requires - // a node identity but none has been generated or loaded. + // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // ErrorMinerManagerNotConfigured is returned when a miner operation is - // attempted but no MinerManager has been set on the Worker. + // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) - diff --git a/node/identity.go b/node/identity.go index fc2aa2d..1309281 100644 --- a/node/identity.go +++ b/node/identity.go @@ -73,9 +73,7 @@ type NodeIdentity struct { Role NodeRole `json:"role"` } -// NodeManager handles node identity operations including key generation and storage. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() type NodeManager struct { identity *NodeIdentity privateKey []byte // Never serialized to JSON @@ -126,7 +124,7 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } -// HasIdentity returns true if a node identity has been initialized. +// hasIdentity := nodeManager.HasIdentity() func (n *NodeManager) HasIdentity() bool { n.mu.RLock() defer n.mu.RUnlock() diff --git a/node/levin/connection.go b/node/levin/connection.go index 46670f2..74a3a04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -10,26 +10,22 @@ import ( "time" ) -// Levin protocol flags. +// flags := FlagRequest | FlagResponse const ( FlagRequest uint32 = 0x00000001 FlagResponse uint32 = 0x00000002 ) -// LevinProtocolVersion is the protocol version field written into every header. +// header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// Default timeout values for Connection read and write operations. +// conn.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// Connection wraps a net.Conn and provides framed Levin packet I/O. -// All writes are serialised by an internal mutex, making it safe to call -// WritePacket and WriteResponse concurrently from multiple goroutines. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -45,9 +41,7 @@ type Connection struct { writeMutex sync.Mutex } -// NewConnection creates a Connection that wraps conn with sensible defaults. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) func NewConnection(conn net.Conn) *Connection { return &Connection{ MaxPayloadSize: MaxPayloadSize, @@ -57,9 +51,7 @@ func NewConnection(conn net.Conn) *Connection { } } -// WritePacket sends a Levin request or notification. -// -// err := conn.WritePacket(CommandPing, payload, true) +// err := conn.WritePacket(CommandPing, payload, true) func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, @@ -73,9 +65,7 @@ func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool return c.writeFrame(&header, payload) } -// WriteResponse sends a Levin response packet with the given return code. -// -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) +// err := conn.WriteResponse(CommandPing, payload, ReturnOK) func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, @@ -113,9 +103,7 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// ReadPacket reads and validates the next Levin packet. -// -// header, payload, err := conn.ReadPacket() +// header, payload, err := conn.ReadPacket() func (c *Connection) ReadPacket() (Header, []byte, error) { if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return Header{}, nil, err @@ -150,16 +138,12 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { return header, payload, nil } -// Close closes the underlying network connection. -// -// err := conn.Close() +// err := conn.Close() func (c *Connection) Close() error { return c.conn.Close() } -// RemoteAddr returns the remote address of the underlying connection as a string. -// -// addr := conn.RemoteAddr() +// addr := conn.RemoteAddr() func (c *Connection) RemoteAddr() string { return c.conn.RemoteAddr().String() } diff --git a/node/message.go b/node/message.go index 4931d8f..1640fde 100644 --- a/node/message.go +++ b/node/message.go @@ -126,9 +126,7 @@ func NewMessage(messageType MessageType, from, to string, payload any) (*Message }, nil } -// Reply creates a response message that points back to the original. -// -// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) +// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) { reply, err := NewMessage(messageType, m.To, m.From, payload) if err != nil { @@ -138,10 +136,8 @@ func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) return reply, nil } -// ParsePayload decodes the payload into the supplied target. -// -// var ping PingPayload -// err := message.ParsePayload(&ping) +// var ping PingPayload +// err := message.ParsePayload(&ping) func (m *Message) ParsePayload(target any) error { if m.Payload == nil { return nil @@ -295,9 +291,7 @@ const ( ErrorCodeTimeout = 1005 ) -// NewErrorMessage builds an error response message for an existing request. -// -// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") +// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { errorMessage, err := NewMessage(MessageError, from, to, ErrorPayload{ Code: code, diff --git a/node/peer.go b/node/peer.go index aeafe82..b44348a 100644 --- a/node/peer.go +++ b/node/peer.go @@ -53,7 +53,7 @@ const peerRegistrySaveDebounceInterval = 5 * time.Second type PeerAuthMode int const ( - // PeerAuthOpen allows any peer to connect (original behavior) + // PeerAuthOpen allows any peer to connect. PeerAuthOpen PeerAuthMode = iota // PeerAuthAllowlist only allows pre-registered peers or those with allowed public keys PeerAuthAllowlist @@ -98,9 +98,7 @@ func validatePeerName(name string) error { return nil } -// PeerRegistry manages known peers with KD-tree based selection. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() type PeerRegistry struct { peers map[string]*Peer kdTree *poindexter.KDTree[string] // KD-tree with peer ID as payload diff --git a/node/protocol.go b/node/protocol.go index 34f255d..72c1624 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -63,7 +63,7 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, return nil } -// DefaultResponseHandler is the default response handler instance. +// handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. diff --git a/node/transport.go b/node/transport.go index 4acdfff..9d9c989 100644 --- a/node/transport.go +++ b/node/transport.go @@ -144,9 +144,7 @@ func (d *MessageDeduplicator) Cleanup() { } } -// Transport manages WebSocket connections with SMSG encryption. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) type Transport struct { config TransportConfig httpServer *http.Server @@ -163,9 +161,7 @@ type Transport struct { waitGroup sync.WaitGroup } -// PeerRateLimiter implements a simple token bucket rate limiter per peer. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) type PeerRateLimiter struct { availableTokens int capacity int @@ -187,7 +183,7 @@ func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { } } -// Allow checks if a message is allowed and consumes a token if so +// allowed := rateLimiter.Allow() func (r *PeerRateLimiter) Allow() bool { r.mutex.Lock() defer r.mutex.Unlock() @@ -209,9 +205,7 @@ func (r *PeerRateLimiter) Allow() bool { return false } -// PeerConnection represents an active connection to a peer. -// -// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} +// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { Peer *Peer Conn *websocket.Conn @@ -224,9 +218,7 @@ type PeerConnection struct { rateLimiter *PeerRateLimiter // Per-peer message rate limiting } -// NewTransport creates a WebSocket transport for a node and peer registry. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) @@ -310,9 +302,7 @@ func (t *Transport) agentUserAgent() string { ) } -// Start opens the WebSocket listener and background maintenance loops. -// -// err := transport.Start() +// err := transport.Start() func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.webSocketPath(), t.handleWebSocketUpgrade) @@ -381,9 +371,7 @@ func (t *Transport) Start() error { return nil } -// Stop closes active connections and shuts the transport down cleanly. -// -// err := transport.Stop() +// err := transport.Stop() func (t *Transport) Stop() error { t.cancelLifecycle() @@ -410,18 +398,14 @@ func (t *Transport) Stop() error { return nil } -// OnMessage installs the handler for incoming messages before Start. -// -// transport.OnMessage(worker.HandleMessage) +// transport.OnMessage(worker.HandleMessage) func (t *Transport) OnMessage(handler MessageHandler) { t.mutex.Lock() defer t.mutex.Unlock() t.messageHandler = handler } -// Connect dials a peer, completes the handshake, and starts the session loops. -// -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -485,9 +469,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return pc, nil } -// Send transmits an encrypted message to a connected peer. -// -// err := transport.Send("worker-1", message) +// err := transport.Send("worker-1", message) func (t *Transport) Send(peerID string, msg *Message) error { t.mutex.RLock() pc, exists := t.connections[peerID] @@ -500,11 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// Connections returns an iterator over all active peer connections. -// -// for pc := range transport.Connections() { -// log.Printf("connected: %s", pc.Peer.ID) -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -518,10 +498,7 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } } -// Broadcast sends a message to every connected peer except the sender. -// The sender (msg.From) is excluded to prevent echo. -// -// err := transport.Broadcast(announcement) +// err := transport.Broadcast(announcement) func (t *Transport) Broadcast(msg *Message) error { conns := slices.Collect(t.Connections()) @@ -537,9 +514,7 @@ func (t *Transport) Broadcast(msg *Message) error { return lastErr } -// GetConnection returns an active connection to a peer. -// -// connection := transport.GetConnection("worker-1") +// connection := transport.GetConnection("worker-1") func (t *Transport) GetConnection(peerID string) *PeerConnection { t.mutex.RLock() defer t.mutex.RUnlock() @@ -973,9 +948,7 @@ func (t *Transport) removeConnection(pc *PeerConnection) { pc.Close() } -// Send sends an encrypted message over the connection. -// -// err := peerConnection.Send(message) +// err := peerConnection.Send(message) func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() @@ -993,9 +966,7 @@ func (pc *PeerConnection) Send(msg *Message) error { return pc.Conn.WriteMessage(websocket.BinaryMessage, data) } -// Close closes the connection. -// -// err := peerConnection.Close() +// err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { @@ -1021,9 +992,7 @@ const ( DisconnectShutdown = 1004 // Server shutdown ) -// GracefulClose sends a disconnect message before closing the connection. -// -// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) +// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { @@ -1090,12 +1059,9 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeerCount returns the number of connected peers. -// -// count := transport.ConnectedPeerCount() +// count := transport.ConnectedPeerCount() func (t *Transport) ConnectedPeerCount() int { t.mutex.RLock() defer t.mutex.RUnlock() return len(t.connections) } - diff --git a/node/worker.go b/node/worker.go index 711c16b..3de7277 100644 --- a/node/worker.go +++ b/node/worker.go @@ -10,10 +10,7 @@ import ( "github.com/adrg/xdg" ) -// MinerManager interface for the mining package integration. -// This allows the node package to interact with mining.Manager without import cycles. -// -// var minerManager MinerManager +// var minerManager MinerManager type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error @@ -21,9 +18,7 @@ type MinerManager interface { GetMiner(name string) (MinerInstance, error) } -// MinerInstance represents a running miner for stats collection. -// -// var miner MinerInstance +// var miner MinerInstance type MinerInstance interface { GetName() string GetType() string @@ -31,17 +26,13 @@ type MinerInstance interface { GetConsoleHistory(lines int) []string } -// ProfileManager interface for profile operations. -// -// var profileManager ProfileManager +// var profileManager ProfileManager type ProfileManager interface { GetProfile(id string) (any, error) SaveProfile(profile any) error } -// Worker handles incoming messages on a worker node. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) type Worker struct { nodeManager *NodeManager transport *Transport @@ -51,9 +42,7 @@ type Worker struct { DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) } -// NewWorker creates a new Worker instance. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { return &Worker{ nodeManager: nodeManager, @@ -63,23 +52,17 @@ func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { } } -// SetMinerManager attaches the miner manager used for miner operations. -// -// worker.SetMinerManager(minerManager) +// worker.SetMinerManager(minerManager) func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager } -// SetProfileManager attaches the profile manager used for profile operations. -// -// worker.SetProfileManager(profileManager) +// worker.SetProfileManager(profileManager) func (w *Worker) SetProfileManager(manager ProfileManager) { w.profileManager = manager } -// HandleMessage routes an incoming message to the correct worker handler. -// -// worker.HandleMessage(peerConnection, message) +// worker.HandleMessage(peerConnection, message) func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) { var response *Message var err error -- 2.45.3 From 4f4785505214245f81f8517c9cc8ab3a53cc06e7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:42:04 +0000 Subject: [PATCH 40/60] fix(transport): serialise graceful websocket close Co-Authored-By: Virgil --- node/transport.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/node/transport.go b/node/transport.go index 9d9c989..155d900 100644 --- a/node/transport.go +++ b/node/transport.go @@ -482,9 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// for pc := range transport.Connections() { -// _ = pc -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -953,6 +953,10 @@ func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + return pc.sendLocked(msg) +} + +func (pc *PeerConnection) sendLocked(msg *Message) error { data, err := pc.transport.encryptMessage(msg, pc.SharedSecret) if err != nil { return err @@ -996,10 +1000,10 @@ const ( func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { + pc.writeMutex.Lock() + defer pc.writeMutex.Unlock() + // Try to send disconnect message (best effort). - // Note: we must NOT call SetWriteDeadline outside writeMutex — Send() - // already manages write deadlines under the lock. Setting it here - // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() if identity != nil { @@ -1009,7 +1013,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) if msgErr == nil { - pc.Send(msg) + _ = pc.sendLocked(msg) } } } -- 2.45.3 From 643b93da01cb4190b1cfff81bb91a18449036784 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:48:17 +0000 Subject: [PATCH 41/60] refactor(p2p): align AX comments and harden UEPS parsing Co-Authored-By: Virgil --- logging/logger.go | 24 +++++++--------- ueps/packet.go | 54 +++++++++++------------------------- ueps/packet_coverage_test.go | 29 +++++++++++++++++++ ueps/reader.go | 37 ++++++++++++------------ 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index bbaac12..0f463b7 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -1,4 +1,4 @@ -// Package logging provides structured logging with log levels and fields. +// logger := New(DefaultConfig()) package logging import ( @@ -15,13 +15,9 @@ import ( type Level int const ( - // LevelDebug is the most verbose log level. LevelDebug Level = iota - // LevelInfo is for general informational messages. LevelInfo - // LevelWarn is for warning messages. LevelWarn - // LevelError is for error messages. LevelError ) @@ -153,47 +149,47 @@ func (l *Logger) log(level Level, message string, fields Fields) { _, _ = l.output.Write([]byte(sb.String())) } -// Debug logs a debug message. +// Debug("connected", Fields{"peer_id": "node-1"}) func (l *Logger) Debug(message string, fields ...Fields) { l.log(LevelDebug, message, mergeFields(fields)) } -// Info logs an informational message. +// Info("worker started", Fields{"component": "transport"}) func (l *Logger) Info(message string, fields ...Fields) { l.log(LevelInfo, message, mergeFields(fields)) } -// Warn logs a warning message. +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func (l *Logger) Warn(message string, fields ...Fields) { l.log(LevelWarn, message, mergeFields(fields)) } -// Error logs an error message. +// Error("send failed", Fields{"peer_id": "node-1"}) func (l *Logger) Error(message string, fields ...Fields) { l.log(LevelError, message, mergeFields(fields)) } -// Debugf logs a formatted debug message. +// Debugf("connected peer %s", "node-1") func (l *Logger) Debugf(format string, args ...any) { l.log(LevelDebug, core.Sprintf(format, args...), nil) } -// Infof logs a formatted informational message. +// Infof("worker %s ready", "node-1") func (l *Logger) Infof(format string, args ...any) { l.log(LevelInfo, core.Sprintf(format, args...), nil) } -// Warnf logs a formatted warning message. +// Warnf("peer %s is slow", "node-1") func (l *Logger) Warnf(format string, args ...any) { l.log(LevelWarn, core.Sprintf(format, args...), nil) } -// Errorf logs a formatted error message. +// Errorf("peer %s failed", "node-1") func (l *Logger) Errorf(format string, args ...any) { l.log(LevelError, core.Sprintf(format, args...), nil) } -// mergeFields combines multiple Fields maps into one. +// fields := mergeFields([]Fields{{"peer_id": "node-1"}, {"attempt": 2}}) func mergeFields(fields []Fields) Fields { if len(fields) == 0 { return nil diff --git a/ueps/packet.go b/ueps/packet.go index 45ed113..f65a7b4 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -10,60 +10,49 @@ import ( core "dappco.re/go/core" ) -// TLV Types const ( TagVersion = 0x01 TagCurrentLayer = 0x02 TagTargetLayer = 0x03 TagIntent = 0x04 TagThreatScore = 0x05 - TagHMAC = 0x06 // The Signature - TagPayload = 0xFF // The Data + TagHMAC = 0x06 + TagPayload = 0xFF ) -// UEPSHeader represents the conscious routing metadata. -// -// header := UEPSHeader{IntentID: 0x01} +// header := UEPSHeader{Version: 0x09, CurrentLayer: 5, TargetLayer: 5, IntentID: 0x01} type UEPSHeader struct { - Version uint8 // Default 0x09 + Version uint8 CurrentLayer uint8 TargetLayer uint8 - IntentID uint8 // Semantic Token - ThreatScore uint16 // 0-65535 + IntentID uint8 + ThreatScore uint16 } -// PacketBuilder builds a signed UEPS frame from a concrete intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) type PacketBuilder struct { Header UEPSHeader Payload []byte } -// NewBuilder creates a packet builder for a specific intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { return &PacketBuilder{ Header: UEPSHeader{ - Version: 0x09, // IPv9 - CurrentLayer: 5, // Application - TargetLayer: 5, // Application + Version: 0x09, + CurrentLayer: 5, + TargetLayer: 5, IntentID: intentID, - ThreatScore: 0, // Assumed innocent until proven guilty + ThreatScore: 0, }, Payload: payload, } } -// MarshalAndSign signs a packet with a shared secret. -// -// frame, err := builder.MarshalAndSign(sharedSecret) +// frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { buf := new(bytes.Buffer) - // 1. Write Standard Header Tags (0x01 - 0x05) - // We write these first because they are part of what we sign. if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } @@ -77,30 +66,21 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return nil, err } - // Threat Score is uint16, needs binary packing tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { return nil, err } - // 2. Calculate HMAC - // The signature covers: Existing Header TLVs + The Payload - // It does NOT cover the HMAC TLV tag itself (obviously) mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) // The headers so far - mac.Write(p.Payload) // The data + mac.Write(buf.Bytes()) + mac.Write(p.Payload) signature := mac.Sum(nil) - // 3. Write HMAC TLV (0x06) - // Length is 32 bytes for SHA256 if err := writeTLV(buf, TagHMAC, signature); err != nil { return nil, err } - // 4. Write Payload TLV (0xFF) - // Fixed: Now uses writeTLV which provides a 2-byte length prefix. - // This prevents the io.ReadAll DoS and allows multiple packets in a stream. if err := writeTLV(buf, TagPayload, p.Payload); err != nil { return nil, err } @@ -108,10 +88,8 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return buf.Bytes(), nil } -// writeTLV writes a single tag-length-value record with a 2-byte length prefix. -// It supports payloads up to 64KB. +// writeTLV(&buf, TagPayload, []byte("hello")) func writeTLV(w io.Writer, tag uint8, value []byte) error { - // Check length constraint (2 byte length = max 65535 bytes) if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } diff --git a/ueps/packet_coverage_test.go b/ueps/packet_coverage_test.go index 6e3eced..ffd2572 100644 --- a/ueps/packet_coverage_test.go +++ b/ueps/packet_coverage_test.go @@ -211,3 +211,32 @@ func TestPacketCoverage_ReadAndVerify_ManualPacket_PayloadReadError_Bad(t *testi require.Error(t, err) assert.Equal(t, io.ErrUnexpectedEOF, err) } + +// TestReadAndVerify_MalformedHeaderTLV_Bad verifies malformed header values +// return an error instead of panicking during TLV reconstruction. +func TestPacketCoverage_ReadAndVerify_MalformedHeaderTLV_Bad(t *testing.T) { + tests := []struct { + name string + frame []byte + wantErr string + }{ + { + name: "ZeroLengthVersion", + frame: []byte{TagVersion, 0x00, 0x00}, + wantErr: "malformed version TLV", + }, + { + name: "ShortThreatScore", + frame: []byte{TagThreatScore, 0x00, 0x01, 0xFF}, + wantErr: "malformed threat score TLV", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(tc.frame)), testSecret) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/ueps/reader.go b/ueps/reader.go index e620ed1..a024c9c 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -11,84 +11,83 @@ import ( core "dappco.re/go/core" ) -// ParsedPacket holds the verified data -// -// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} +// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} type ParsedPacket struct { Header UEPSHeader Payload []byte } -// ReadAndVerify reads a UEPS frame from the stream and validates the HMAC. -// It consumes the stream up to the end of the packet. -// -// packet, err := ReadAndVerify(reader, sharedSecret) +// packet, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(frame)), sharedSecret) func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) { - // Buffer to reconstruct the data for HMAC verification var signedData bytes.Buffer header := UEPSHeader{} var signature []byte var payload []byte - // Loop through TLVs for { - // 1. Read Tag tag, err := r.ReadByte() if err != nil { return nil, err } - // 2. Read Length (2-byte big-endian uint16) lenBuf := make([]byte, 2) if _, err := io.ReadFull(r, lenBuf); err != nil { return nil, err } length := int(binary.BigEndian.Uint16(lenBuf)) - // 3. Read Value value := make([]byte, length) if _, err := io.ReadFull(r, value); err != nil { return nil, err } - // 4. Handle Tag switch tag { case TagVersion: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed version TLV", nil) + } header.Version = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagCurrentLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed current layer TLV", nil) + } header.CurrentLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagTargetLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed target layer TLV", nil) + } header.TargetLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagIntent: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed intent TLV", nil) + } header.IntentID = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagThreatScore: + if len(value) != 2 { + return nil, core.E("ueps.ReadAndVerify", "malformed threat score TLV", nil) + } header.ThreatScore = binary.BigEndian.Uint16(value) signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagHMAC: signature = value - // HMAC tag itself is not part of the signed data case TagPayload: payload = value - // Exit loop after payload (last tag in UEPS frame) - // Note: The HMAC covers the Payload but NOT the TagPayload/Length bytes - // to match the PacketBuilder.MarshalAndSign logic. goto verify default: - // Unknown tag (future proofing), verify it but ignore semantics signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) @@ -100,8 +99,6 @@ verify: return nil, core.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil) } - // 5. Verify HMAC - // Reconstruct: Headers (signedData) + Payload mac := hmac.New(sha256.New, sharedSecret) mac.Write(signedData.Bytes()) mac.Write(payload) -- 2.45.3 From 723f71143e933933d5af4b10e02ba8617c490492 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:56:56 +0000 Subject: [PATCH 42/60] refactor(node): align AX naming across transport and protocol helpers Co-Authored-By: Virgil --- node/buffer_pool.go | 18 +++++----- node/levin/connection.go | 74 ++++++++++++++++++++-------------------- node/levin/header.go | 52 ++++++++++++++-------------- node/levin/storage.go | 56 +++++++++++++++--------------- node/levin/varint.go | 60 ++++++++++++++++---------------- node/transport.go | 60 ++++++++++++++++---------------- ueps/packet.go | 34 +++++++++--------- 7 files changed, 177 insertions(+), 177 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index ace5915..91f160d 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -17,16 +17,16 @@ var bufferPool = sync.Pool{ // getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { - buf := bufferPool.Get().(*bytes.Buffer) - buf.Reset() - return buf + buffer := bufferPool.Get().(*bytes.Buffer) + buffer.Reset() + return buffer } // putBuffer returns a buffer to the pool. -func putBuffer(buf *bytes.Buffer) { +func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) - if buf.Cap() <= 65536 { - bufferPool.Put(buf) + if buffer.Cap() <= 65536 { + bufferPool.Put(buffer) } } @@ -34,9 +34,9 @@ func putBuffer(buf *bytes.Buffer) { // restores the historical no-EscapeHTML behaviour expected by the node package. // Returns a copy of the encoded bytes (safe to use after the function returns). // -// data, err := MarshalJSON(v) -func MarshalJSON(v any) ([]byte, error) { - encoded := core.JSONMarshal(v) +// data, err := MarshalJSON(value) +func MarshalJSON(value any) ([]byte, error) { + encoded := core.JSONMarshal(value) if !encoded.OK { return nil, encoded.Value.(error) } diff --git a/node/levin/connection.go b/node/levin/connection.go index 74a3a04..76bd643 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -19,13 +19,13 @@ const ( // header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// conn.ReadTimeout = DefaultReadTimeout +// connection.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// conn := NewConnection(netConn) +// connection := NewConnection(networkConnection) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -37,65 +37,65 @@ type Connection struct { // WriteTimeout is the deadline applied before each write call. WriteTimeout time.Duration - conn net.Conn - writeMutex sync.Mutex + networkConnection net.Conn + writeMutex sync.Mutex } -// conn := NewConnection(netConn) -func NewConnection(conn net.Conn) *Connection { +// connection := NewConnection(networkConnection) +func NewConnection(connection net.Conn) *Connection { return &Connection{ - MaxPayloadSize: MaxPayloadSize, - ReadTimeout: DefaultReadTimeout, - WriteTimeout: DefaultWriteTimeout, - conn: conn, + MaxPayloadSize: MaxPayloadSize, + ReadTimeout: DefaultReadTimeout, + WriteTimeout: DefaultWriteTimeout, + networkConnection: connection, } } -// err := conn.WritePacket(CommandPing, payload, true) -func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { +// err := connection.WritePacket(CommandPing, payload, true) +func (connection *Connection) WritePacket(commandID uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: expectResponse, - Command: cmd, + Command: commandID, ReturnCode: ReturnOK, Flags: FlagRequest, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) -func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { +// err := connection.WriteResponse(CommandPing, payload, ReturnOK) +func (connection *Connection) WriteResponse(commandID uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: false, - Command: cmd, + Command: commandID, ReturnCode: returnCode, Flags: FlagResponse, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } // writeFrame serialises header + payload and writes them atomically. -func (c *Connection) writeFrame(header *Header, payload []byte) error { - buf := EncodeHeader(header) +func (connection *Connection) writeFrame(header *Header, payload []byte) error { + headerBytes := EncodeHeader(header) - c.writeMutex.Lock() - defer c.writeMutex.Unlock() + connection.writeMutex.Lock() + defer connection.writeMutex.Unlock() - if err := c.conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { + if err := connection.networkConnection.SetWriteDeadline(time.Now().Add(connection.WriteTimeout)); err != nil { return err } - if _, err := c.conn.Write(buf[:]); err != nil { + if _, err := connection.networkConnection.Write(headerBytes[:]); err != nil { return err } if len(payload) > 0 { - if _, err := c.conn.Write(payload); err != nil { + if _, err := connection.networkConnection.Write(payload); err != nil { return err } } @@ -103,15 +103,15 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// header, payload, err := conn.ReadPacket() -func (c *Connection) ReadPacket() (Header, []byte, error) { - if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { +// header, payload, err := connection.ReadPacket() +func (connection *Connection) ReadPacket() (Header, []byte, error) { + if err := connection.networkConnection.SetReadDeadline(time.Now().Add(connection.ReadTimeout)); err != nil { return Header{}, nil, err } // Read header. var headerBytes [HeaderSize]byte - if _, err := io.ReadFull(c.conn, headerBytes[:]); err != nil { + if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err } @@ -121,7 +121,7 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } // Check against the connection-specific payload limit. - if header.PayloadSize > c.MaxPayloadSize { + if header.PayloadSize > connection.MaxPayloadSize { return Header{}, nil, ErrorPayloadTooBig } @@ -131,19 +131,19 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } payload := make([]byte, header.PayloadSize) - if _, err := io.ReadFull(c.conn, payload); err != nil { + if _, err := io.ReadFull(connection.networkConnection, payload); err != nil { return Header{}, nil, err } return header, payload, nil } -// err := conn.Close() -func (c *Connection) Close() error { - return c.conn.Close() +// err := connection.Close() +func (connection *Connection) Close() error { + return connection.networkConnection.Close() } -// addr := conn.RemoteAddr() -func (c *Connection) RemoteAddr() string { - return c.conn.RemoteAddr().String() +// addr := connection.RemoteAddr() +func (connection *Connection) RemoteAddr() string { + return connection.networkConnection.RemoteAddr().String() } diff --git a/node/levin/header.go b/node/levin/header.go index 2f69577..ee4d9ab 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -60,43 +60,43 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises h into a fixed-size 33-byte array (little-endian). +// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). // // encoded := EncodeHeader(header) -func EncodeHeader(h *Header) [HeaderSize]byte { - var buf [HeaderSize]byte - binary.LittleEndian.PutUint64(buf[0:8], h.Signature) - binary.LittleEndian.PutUint64(buf[8:16], h.PayloadSize) - if h.ExpectResponse { - buf[16] = 0x01 +func EncodeHeader(header *Header) [HeaderSize]byte { + var headerBytes [HeaderSize]byte + binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) + binary.LittleEndian.PutUint64(headerBytes[8:16], header.PayloadSize) + if header.ExpectResponse { + headerBytes[16] = 0x01 } else { - buf[16] = 0x00 + headerBytes[16] = 0x00 } - binary.LittleEndian.PutUint32(buf[17:21], h.Command) - binary.LittleEndian.PutUint32(buf[21:25], uint32(h.ReturnCode)) - binary.LittleEndian.PutUint32(buf[25:29], h.Flags) - binary.LittleEndian.PutUint32(buf[29:33], h.ProtocolVersion) - return buf + binary.LittleEndian.PutUint32(headerBytes[17:21], header.Command) + binary.LittleEndian.PutUint32(headerBytes[21:25], uint32(header.ReturnCode)) + binary.LittleEndian.PutUint32(headerBytes[25:29], header.Flags) + binary.LittleEndian.PutUint32(headerBytes[29:33], header.ProtocolVersion) + return headerBytes } // DecodeHeader deserialises a 33-byte array into a Header, validating // the magic signature. // -// header, err := DecodeHeader(buf) -func DecodeHeader(buf [HeaderSize]byte) (Header, error) { - var h Header - h.Signature = binary.LittleEndian.Uint64(buf[0:8]) - if h.Signature != Signature { +// header, err := DecodeHeader(headerBytes) +func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { + var header Header + header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) + if header.Signature != Signature { return Header{}, ErrorBadSignature } - h.PayloadSize = binary.LittleEndian.Uint64(buf[8:16]) - if h.PayloadSize > MaxPayloadSize { + header.PayloadSize = binary.LittleEndian.Uint64(headerBytes[8:16]) + if header.PayloadSize > MaxPayloadSize { return Header{}, ErrorPayloadTooBig } - h.ExpectResponse = buf[16] == 0x01 - h.Command = binary.LittleEndian.Uint32(buf[17:21]) - h.ReturnCode = int32(binary.LittleEndian.Uint32(buf[21:25])) - h.Flags = binary.LittleEndian.Uint32(buf[25:29]) - h.ProtocolVersion = binary.LittleEndian.Uint32(buf[29:33]) - return h, nil + header.ExpectResponse = headerBytes[16] == 0x01 + header.Command = binary.LittleEndian.Uint32(headerBytes[17:21]) + header.ReturnCode = int32(binary.LittleEndian.Uint32(headerBytes[21:25])) + header.Flags = binary.LittleEndian.Uint32(headerBytes[25:29]) + header.ProtocolVersion = binary.LittleEndian.Uint32(headerBytes[29:33]) + return header, nil } diff --git a/node/levin/storage.go b/node/levin/storage.go index a6c74e2..9a6d4ed 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -83,62 +83,62 @@ type Value struct { // Uint64Value creates a Value of TypeUint64. // // value := Uint64Value(42) -func Uint64Value(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } +func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } // Uint32Value creates a Value of TypeUint32. // // value := Uint32Value(42) -func Uint32Value(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } +func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } // Uint16Value creates a Value of TypeUint16. // // value := Uint16Value(42) -func Uint16Value(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } +func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } // Uint8Value creates a Value of TypeUint8. // // value := Uint8Value(42) -func Uint8Value(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } +func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } // Int64Value creates a Value of TypeInt64. // // value := Int64Value(42) -func Int64Value(v int64) Value { return Value{Type: TypeInt64, intVal: v} } +func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } // Int32Value creates a Value of TypeInt32. // // value := Int32Value(42) -func Int32Value(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } +func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } // Int16Value creates a Value of TypeInt16. // // value := Int16Value(42) -func Int16Value(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } +func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } // Int8Value creates a Value of TypeInt8. // // value := Int8Value(42) -func Int8Value(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } +func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } // BoolValue creates a Value of TypeBool. // // value := BoolValue(true) -func BoolValue(v bool) Value { return Value{Type: TypeBool, boolVal: v} } +func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } // DoubleValue creates a Value of TypeDouble. // // value := DoubleValue(3.14) -func DoubleValue(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } +func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } // StringValue creates a Value of TypeString. The slice is not copied. // // value := StringValue([]byte("hello")) -func StringValue(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } +func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } // ObjectValue creates a Value of TypeObject wrapping a nested Section. // // value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) -func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} } +func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors @@ -147,29 +147,29 @@ func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} // Uint64ArrayValue creates a typed array of uint64 values. // // value := Uint64ArrayValue([]uint64{1, 2, 3}) -func Uint64ArrayValue(vs []uint64) Value { - return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs} +func Uint64ArrayValue(values []uint64) Value { + return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } // Uint32ArrayValue creates a typed array of uint32 values. // // value := Uint32ArrayValue([]uint32{1, 2, 3}) -func Uint32ArrayValue(vs []uint32) Value { - return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs} +func Uint32ArrayValue(values []uint32) Value { + return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } // StringArrayValue creates a typed array of byte-string values. // // value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) -func StringArrayValue(vs [][]byte) Value { - return Value{Type: ArrayFlag | TypeString, stringArray: vs} +func StringArrayValue(values [][]byte) Value { + return Value{Type: ArrayFlag | TypeString, stringArray: values} } // ObjectArrayValue creates a typed array of Section values. // // value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) -func ObjectArrayValue(vs []Section) Value { - return Value{Type: ArrayFlag | TypeObject, objectArray: vs} +func ObjectArrayValue(values []Section) Value { + return Value{Type: ArrayFlag | TypeObject, objectArray: values} } // --------------------------------------------------------------------------- @@ -317,25 +317,25 @@ func (v Value) AsSectionArray() ([]Section, error) { // deterministic output. // // data, err := EncodeStorage(section) -func EncodeStorage(s Section) ([]byte, error) { - buf := make([]byte, 0, 256) +func EncodeStorage(section Section) ([]byte, error) { + buffer := make([]byte, 0, 256) // 9-byte storage header. var hdr [StorageHeaderSize]byte binary.LittleEndian.PutUint32(hdr[0:4], StorageSignatureA) binary.LittleEndian.PutUint32(hdr[4:8], StorageSignatureB) hdr[8] = StorageVersion - buf = append(buf, hdr[:]...) + buffer = append(buffer, hdr[:]...) // Encode root section. - out, err := encodeSection(buf, s) + out, err := encodeSection(buffer, section) if err != nil { return nil, err } return out, nil } -// encodeSection appends a section (entry count + entries) to buf. +// encodeSection appends a section (entry count + entries) to buffer. func encodeSection(buf []byte, s Section) ([]byte, error) { // Sort keys for deterministic output. keys := slices.Sorted(maps.Keys(s)) @@ -510,7 +510,7 @@ func DecodeStorage(data []byte) (Section, error) { return s, err } -// decodeSection reads a section from buf and returns the section plus +// decodeSection reads a section from buffer and returns the section plus // the number of bytes consumed. func decodeSection(buf []byte) (Section, int, error) { count, n, err := UnpackVarint(buf) @@ -556,7 +556,7 @@ func decodeSection(buf []byte) (Section, int, error) { return s, off, nil } -// decodeValue reads a value of the given type tag from buf and returns +// decodeValue reads a value of the given type tag from buffer and returns // the value plus bytes consumed. func decodeValue(buf []byte, tag uint8) (Value, int, error) { // Array types. @@ -656,7 +656,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { } } -// decodeArray reads a typed array from buf (tag has ArrayFlag set). +// decodeArray reads a typed array from buffer (tag has ArrayFlag set). func decodeArray(buf []byte, tag uint8) (Value, int, error) { elemType := tag & ^ArrayFlag diff --git a/node/levin/varint.go b/node/levin/varint.go index 5160646..2d26512 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,67 +28,67 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes v using the epee portable-storage varint scheme. +// PackVarint encodes value using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; // the remaining bits carry the value in little-endian order. // // encoded := PackVarint(42) -func PackVarint(v uint64) []byte { +func PackVarint(value uint64) []byte { switch { - case v <= varintMax1: - return []byte{byte((v << 2) | varintMark1)} - case v <= varintMax2: - raw := uint16((v << 2) | varintMark2) - buf := make([]byte, 2) - binary.LittleEndian.PutUint16(buf, raw) - return buf - case v <= varintMax4: - raw := uint32((v << 2) | varintMark4) - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, raw) - return buf + case value <= varintMax1: + return []byte{byte((value << 2) | varintMark1)} + case value <= varintMax2: + raw := uint16((value << 2) | varintMark2) + buffer := make([]byte, 2) + binary.LittleEndian.PutUint16(buffer, raw) + return buffer + case value <= varintMax4: + raw := uint32((value << 2) | varintMark4) + buffer := make([]byte, 4) + binary.LittleEndian.PutUint32(buffer, raw) + return buffer default: - raw := (v << 2) | varintMark8 - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, raw) - return buf + raw := (value << 2) | varintMark8 + buffer := make([]byte, 8) + binary.LittleEndian.PutUint64(buffer, raw) + return buffer } } -// UnpackVarint decodes one epee portable-storage varint from buf. +// UnpackVarint decodes one epee portable-storage varint from buffer. // It returns the decoded value, the number of bytes consumed, and any error. // -// value, err := UnpackVarint(data) -func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { - if len(buf) == 0 { +// value, err := UnpackVarint(buffer) +func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { + if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated } - mark := buf[0] & varintMask + mark := buffer[0] & varintMask switch mark { case varintMark1: - value = uint64(buf[0]) >> 2 + value = uint64(buffer[0]) >> 2 return value, 1, nil case varintMark2: - if len(buf) < 2 { + if len(buffer) < 2 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint16(buf[:2]) + raw := binary.LittleEndian.Uint16(buffer[:2]) value = uint64(raw) >> 2 return value, 2, nil case varintMark4: - if len(buf) < 4 { + if len(buffer) < 4 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint32(buf[:4]) + raw := binary.LittleEndian.Uint32(buffer[:4]) value = uint64(raw) >> 2 return value, 4, nil case varintMark8: - if len(buf) < 8 { + if len(buffer) < 8 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint64(buf[:8]) + raw := binary.LittleEndian.Uint64(buffer[:8]) value = raw >> 2 return value, 8, nil default: diff --git a/node/transport.go b/node/transport.go index 155d900..f580073 100644 --- a/node/transport.go +++ b/node/transport.go @@ -41,7 +41,7 @@ const ( // TransportConfig configures the WebSocket transport. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -55,7 +55,7 @@ type TransportConfig struct { // DefaultTransportConfig returns sensible defaults. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -219,13 +219,13 @@ type PeerConnection struct { } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) -func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { +func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) return &Transport{ config: config, - nodeManager: node, - peerRegistry: registry, + nodeManager: nodeManager, + peerRegistry: peerRegistry, connections: make(map[string]*PeerConnection), messageDeduplicator: NewMessageDeduplicator(5 * time.Minute), // 5 minute TTL for dedup upgrader: websocket.Upgrader{ @@ -405,7 +405,7 @@ func (t *Transport) OnMessage(handler MessageHandler) { t.messageHandler = handler } -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// peerConnection, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -426,7 +426,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return nil, core.E("Transport.Connect", "failed to connect to peer", err) } - pc := &PeerConnection{ + peerConnection := &PeerConnection{ Peer: peer, Conn: conn, LastActivity: time.Now(), @@ -435,63 +435,63 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } - // Perform handshake with challenge-response authentication - // This also derives and stores the shared secret in pc.SharedSecret - if err := t.performHandshake(pc); err != nil { + // Perform handshake with challenge-response authentication. + // This also derives and stores the shared secret in peerConnection.SharedSecret. + if err := t.performHandshake(peerConnection); err != nil { conn.Close() return nil, core.E("Transport.Connect", "handshake failed", err) } // Store connection using the real peer ID from handshake t.mutex.Lock() - t.connections[pc.Peer.ID] = pc + t.connections[peerConnection.Peer.ID] = peerConnection t.mutex.Unlock() - logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) + logging.Debug("connected to peer", logging.Fields{"peer_id": peerConnection.Peer.ID, "secret_len": len(peerConnection.SharedSecret)}) logging.Debug("connected peer metadata", logging.Fields{ - "peer_id": pc.Peer.ID, - "user_agent": pc.UserAgent, + "peer_id": peerConnection.Peer.ID, + "user_agent": peerConnection.UserAgent, }) // Update registry - t.peerRegistry.SetConnected(pc.Peer.ID, true) + t.peerRegistry.SetConnected(peerConnection.Peer.ID, true) // Start read loop t.waitGroup.Add(1) - go t.readLoop(pc) + go t.readLoop(peerConnection) - logging.Debug("started readLoop for peer", logging.Fields{"peer_id": pc.Peer.ID}) + logging.Debug("started readLoop for peer", logging.Fields{"peer_id": peerConnection.Peer.ID}) // Start keepalive t.waitGroup.Add(1) - go t.keepalive(pc) + go t.keepalive(peerConnection) - return pc, nil + return peerConnection, nil } // err := transport.Send("worker-1", message) -func (t *Transport) Send(peerID string, msg *Message) error { +func (t *Transport) Send(peerID string, message *Message) error { t.mutex.RLock() - pc, exists := t.connections[peerID] + peerConnection, exists := t.connections[peerID] t.mutex.RUnlock() if !exists { return core.E("Transport.Send", "peer "+peerID+" not connected", nil) } - return pc.Send(msg) + return peerConnection.Send(message) } -// for pc := range transport.Connections() { -// _ = pc +// for peerConnection := range transport.Connections() { +// _ = peerConnection // } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() defer t.mutex.RUnlock() - for _, pc := range t.connections { - if !yield(pc) { + for _, peerConnection := range t.connections { + if !yield(peerConnection) { return } } @@ -499,15 +499,15 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } // err := transport.Broadcast(announcement) -func (t *Transport) Broadcast(msg *Message) error { +func (t *Transport) Broadcast(message *Message) error { conns := slices.Collect(t.Connections()) var lastErr error - for _, pc := range conns { - if pc.Peer != nil && pc.Peer.ID == msg.From { + for _, peerConnection := range conns { + if peerConnection.Peer != nil && peerConnection.Peer.ID == message.From { continue } - if err := pc.Send(msg); err != nil { + if err := peerConnection.Send(message); err != nil { lastErr = err } } diff --git a/ueps/packet.go b/ueps/packet.go index f65a7b4..ad50f9c 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -51,60 +51,60 @@ func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { // frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { - buf := new(bytes.Buffer) + buffer := new(bytes.Buffer) - if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { + if err := writeTLV(buffer, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } - if err := writeTLV(buf, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { + if err := writeTLV(buffer, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { + if err := writeTLV(buffer, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagIntent, []byte{p.Header.IntentID}); err != nil { + if err := writeTLV(buffer, TagIntent, []byte{p.Header.IntentID}); err != nil { return nil, err } - tsBuf := make([]byte, 2) - binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) - if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { + threatScoreBytes := make([]byte, 2) + binary.BigEndian.PutUint16(threatScoreBytes, p.Header.ThreatScore) + if err := writeTLV(buffer, TagThreatScore, threatScoreBytes); err != nil { return nil, err } mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) + mac.Write(buffer.Bytes()) mac.Write(p.Payload) signature := mac.Sum(nil) - if err := writeTLV(buf, TagHMAC, signature); err != nil { + if err := writeTLV(buffer, TagHMAC, signature); err != nil { return nil, err } - if err := writeTLV(buf, TagPayload, p.Payload); err != nil { + if err := writeTLV(buffer, TagPayload, p.Payload); err != nil { return nil, err } - return buf.Bytes(), nil + return buffer.Bytes(), nil } -// writeTLV(&buf, TagPayload, []byte("hello")) -func writeTLV(w io.Writer, tag uint8, value []byte) error { +// writeTLV(&buffer, TagPayload, []byte("hello")) +func writeTLV(writer io.Writer, tag uint8, value []byte) error { if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } - if _, err := w.Write([]byte{tag}); err != nil { + if _, err := writer.Write([]byte{tag}); err != nil { return err } lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(len(value))) - if _, err := w.Write(lenBuf); err != nil { + if _, err := writer.Write(lenBuf); err != nil { return err } - if _, err := w.Write(value); err != nil { + if _, err := writer.Write(value); err != nil { return err } return nil -- 2.45.3 From 3733e61962d1c7454b88e3aa8421ca1cdd246eab Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:04:41 +0000 Subject: [PATCH 43/60] refactor(node): tighten AX comments across public APIs Co-Authored-By: Virgil --- node/bundle.go | 36 +++-------- node/controller.go | 36 +++-------- node/dispatcher.go | 13 ++-- node/identity.go | 47 ++++----------- node/levin/header.go | 13 +--- node/levin/storage.go | 117 +++++++++++------------------------- node/levin/varint.go | 11 +--- node/message.go | 82 +++++++------------------ node/peer.go | 136 ++++++++++++++---------------------------- node/protocol.go | 24 ++------ node/transport.go | 29 +++------ 11 files changed, 156 insertions(+), 388 deletions(-) diff --git a/node/bundle.go b/node/bundle.go index de52eec..f1c7342 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -14,9 +14,7 @@ import ( "forge.lthn.ai/Snider/Borg/pkg/tim" ) -// BundleType defines the type of deployment bundle. -// -// bundleType := BundleProfile +// bundleType := BundleProfile type BundleType string const ( @@ -28,9 +26,7 @@ const ( BundleFull BundleType = "full" ) -// Bundle represents a deployment bundle for P2P transfer. -// -// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} +// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} type Bundle struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -38,9 +34,7 @@ type Bundle struct { Checksum string `json:"checksum"` // SHA-256 of Data } -// BundleManifest describes the contents of a bundle. -// -// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} +// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} type BundleManifest struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -50,9 +44,7 @@ type BundleManifest struct { CreatedAt string `json:"createdAt"` } -// CreateProfileBundle creates an encrypted bundle containing a mining profile. -// -// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") +// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config timBundle, err := tim.New() @@ -78,9 +70,7 @@ func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bun }, nil } -// CreateProfileBundleUnencrypted creates a plain JSON bundle (for testing or trusted networks). -// -// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") +// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error) { checksum := calculateChecksum(profileJSON) @@ -92,9 +82,7 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e }, nil } -// CreateMinerBundle creates an encrypted bundle containing a miner binary and optional profile. -// -// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") +// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary minerContent, err := filesystemRead(minerPath) @@ -144,9 +132,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo }, nil } -// ExtractProfileBundle decrypts and extracts a profile bundle. -// -// profileJSON, err := ExtractProfileBundle(bundle, "password") +// profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -167,9 +153,7 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { return timBundle.Config, nil } -// ExtractMinerBundle decrypts and extracts a miner bundle, returning the miner path and profile. -// -// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") +// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -197,9 +181,7 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string return minerPath, timBundle.Config, nil } -// VerifyBundle checks if a bundle's checksum is valid. -// -// ok := VerifyBundle(bundle) +// ok := VerifyBundle(bundle) func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } diff --git a/node/controller.go b/node/controller.go index 3d4ebc9..17bbc22 100644 --- a/node/controller.go +++ b/node/controller.go @@ -21,9 +21,7 @@ type Controller struct { pendingRequests map[string]chan *Message // message ID -> response channel } -// NewController wires a controller to a node manager, peer registry, and transport. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transport *Transport) *Controller { c := &Controller{ nodeManager: nodeManager, @@ -117,9 +115,7 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D } } -// GetRemoteStats requests miner statistics from a remote peer. -// -// stats, err := controller.GetRemoteStats("worker-1") +// stats, err := controller.GetRemoteStats("worker-1") func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -144,9 +140,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } -// StartRemoteMiner requests a remote peer to start a miner with a given profile. -// -// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) +// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -185,9 +179,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi return nil } -// StopRemoteMiner requests a remote peer to stop a miner. -// -// err := controller.StopRemoteMiner("worker-1", "xmrig-0") +// err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -220,9 +212,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return nil } -// GetRemoteLogs requests console logs from a remote miner. -// -// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) +// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -252,9 +242,7 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// GetAllStats fetches stats from all connected peers. -// -// statsByPeerID := controller.GetAllStats() +// statsByPeerID := controller.GetAllStats() func (c *Controller) GetAllStats() map[string]*StatsPayload { results := make(map[string]*StatsPayload) var mu sync.Mutex @@ -283,9 +271,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// PingPeer sends a ping to a peer and refreshes that peer's metrics. -// -// rttMilliseconds, err := controller.PingPeer("worker-1") +// rttMilliseconds, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -323,9 +309,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return rtt, nil } -// ConnectToPeer opens a transport connection to a peer. -// -// err := controller.ConnectToPeer("worker-1") +// err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { peer := c.peerRegistry.GetPeer(peerID) if peer == nil { @@ -336,9 +320,7 @@ func (c *Controller) ConnectToPeer(peerID string) error { return err } -// DisconnectFromPeer closes the active connection to a peer. -// -// err := controller.DisconnectFromPeer("worker-1") +// err := controller.DisconnectFromPeer("worker-1") func (c *Controller) DisconnectFromPeer(peerID string) error { conn := c.transport.GetConnection(peerID) if conn == nil { diff --git a/node/dispatcher.go b/node/dispatcher.go index 1307713..faedf4b 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -22,10 +22,7 @@ const ( IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) -// IntentHandler processes a UEPS packet that has been routed by intent. -// Implementations receive the fully parsed and HMAC-verified packet. -// -// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } +// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error // dispatcher := NewDispatcher() @@ -58,10 +55,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// for intentID, handler := range dispatcher.Handlers() { -// _ = intentID -// _ = handler -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() diff --git a/node/identity.go b/node/identity.go index 1309281..ae5d1f1 100644 --- a/node/identity.go +++ b/node/identity.go @@ -16,12 +16,10 @@ import ( "github.com/adrg/xdg" ) -// ChallengeSize is the size of the challenge in bytes +// challenge := make([]byte, ChallengeSize) const ChallengeSize = 32 -// GenerateChallenge creates a random challenge for authentication. -// -// challenge, err := GenerateChallenge() +// challenge, err := GenerateChallenge() func GenerateChallenge() ([]byte, error) { challenge := make([]byte, ChallengeSize) if _, err := rand.Read(challenge); err != nil { @@ -30,27 +28,20 @@ func GenerateChallenge() ([]byte, error) { return challenge, nil } -// SignChallenge creates an HMAC signature of a challenge using a shared secret. -// The signature proves possession of the shared secret without revealing it. -// -// signature := SignChallenge(challenge, sharedSecret) +// signature := SignChallenge(challenge, sharedSecret) func SignChallenge(challenge []byte, sharedSecret []byte) []byte { mac := hmac.New(sha256.New, sharedSecret) mac.Write(challenge) return mac.Sum(nil) } -// VerifyChallenge verifies that a challenge response was signed with the correct shared secret. -// -// ok := VerifyChallenge(challenge, signature, sharedSecret) +// ok := VerifyChallenge(challenge, signature, sharedSecret) func VerifyChallenge(challenge, response, sharedSecret []byte) bool { expected := SignChallenge(challenge, sharedSecret) return hmac.Equal(response, expected) } -// NodeRole defines the operational mode of a node. -// -// role := RoleWorker +// role := RoleWorker type NodeRole string const ( @@ -62,9 +53,7 @@ const ( RoleDual NodeRole = "dual" ) -// NodeIdentity represents the public identity of a node. -// -// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} +// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} type NodeIdentity struct { ID string `json:"id"` // Derived from public key (first 16 bytes hex) Name string `json:"name"` // Human-friendly name @@ -83,9 +72,7 @@ type NodeManager struct { mu sync.RWMutex } -// NewNodeManager loads the default node identity store. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() func NewNodeManager() (*NodeManager, error) { keyPath, err := xdg.DataFile("lethean-desktop/node/private.key") if err != nil { @@ -100,12 +87,10 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// NewNodeManagerFromPaths loads or creates a node identity store at explicit paths. -// // Missing files are treated as a fresh install; malformed or partial identity // state is returned as an error so callers can handle it explicitly. // -// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, @@ -131,9 +116,7 @@ func (n *NodeManager) HasIdentity() bool { return n.identity != nil } -// GetIdentity returns a copy of the loaded node identity. -// -// identity := nodeManager.GetIdentity() +// identity := nodeManager.GetIdentity() func (n *NodeManager) GetIdentity() *NodeIdentity { n.mu.RLock() defer n.mu.RUnlock() @@ -145,9 +128,7 @@ func (n *NodeManager) GetIdentity() *NodeIdentity { return &identity } -// GenerateIdentity writes a new node identity for the given name and role. -// -// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) +// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { n.mu.Lock() defer n.mu.Unlock() @@ -187,9 +168,7 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { return nil } -// DeriveSharedSecret hashes an X25519 shared secret for use as a symmetric key. -// -// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) +// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error) { n.mu.RLock() defer n.mu.RUnlock() @@ -292,9 +271,7 @@ func (n *NodeManager) loadIdentity() error { return nil } -// Delete removes the node identity and key files from disk. -// -// err := nodeManager.Delete() +// err := nodeManager.Delete() func (n *NodeManager) Delete() error { n.mu.Lock() defer n.mu.Unlock() diff --git a/node/levin/header.go b/node/levin/header.go index ee4d9ab..911435a 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -47,9 +47,7 @@ var ( ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) -// Header is the 33-byte packed header that prefixes every Levin message. -// -// header := Header{Command: CommandHandshake, ExpectResponse: true} +// header := Header{Command: CommandHandshake, ExpectResponse: true} type Header struct { Signature uint64 PayloadSize uint64 @@ -60,9 +58,7 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). -// -// encoded := EncodeHeader(header) +// encoded := EncodeHeader(header) func EncodeHeader(header *Header) [HeaderSize]byte { var headerBytes [HeaderSize]byte binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) @@ -79,10 +75,7 @@ func EncodeHeader(header *Header) [HeaderSize]byte { return headerBytes } -// DecodeHeader deserialises a 33-byte array into a Header, validating -// the magic signature. -// -// header, err := DecodeHeader(headerBytes) +// header, err := DecodeHeader(headerBytes) func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { var header Header header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) diff --git a/node/levin/storage.go b/node/levin/storage.go index 9a6d4ed..08aa7ca 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -48,16 +48,10 @@ var ( ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) ) -// Section is an ordered map of named values forming a portable storage section. -// Field iteration order is always alphabetical by key for deterministic encoding. -// -// section := Section{"id": StringValue([]byte("peer-1"))} +// section := Section{"id": StringValue([]byte("peer-1"))} type Section map[string]Value -// Value holds a typed portable storage value. Use the constructor functions -// (Uint64Value, StringValue, ObjectValue, etc.) to create instances. -// -// value := StringValue([]byte("peer-1")) +// value := StringValue([]byte("peer-1")) type Value struct { Type uint8 @@ -80,94 +74,62 @@ type Value struct { // Scalar constructors // --------------------------------------------------------------------------- -// Uint64Value creates a Value of TypeUint64. -// -// value := Uint64Value(42) +// value := Uint64Value(42) func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } -// Uint32Value creates a Value of TypeUint32. -// -// value := Uint32Value(42) +// value := Uint32Value(42) func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } -// Uint16Value creates a Value of TypeUint16. -// -// value := Uint16Value(42) +// value := Uint16Value(42) func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } -// Uint8Value creates a Value of TypeUint8. -// -// value := Uint8Value(42) +// value := Uint8Value(42) func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } -// Int64Value creates a Value of TypeInt64. -// -// value := Int64Value(42) +// value := Int64Value(42) func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } -// Int32Value creates a Value of TypeInt32. -// -// value := Int32Value(42) +// value := Int32Value(42) func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } -// Int16Value creates a Value of TypeInt16. -// -// value := Int16Value(42) +// value := Int16Value(42) func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } -// Int8Value creates a Value of TypeInt8. -// -// value := Int8Value(42) +// value := Int8Value(42) func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } -// BoolValue creates a Value of TypeBool. -// -// value := BoolValue(true) +// value := BoolValue(true) func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } -// DoubleValue creates a Value of TypeDouble. -// -// value := DoubleValue(3.14) +// value := DoubleValue(3.14) func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } -// StringValue creates a Value of TypeString. The slice is not copied. -// -// value := StringValue([]byte("hello")) +// value := StringValue([]byte("hello")) func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } -// ObjectValue creates a Value of TypeObject wrapping a nested Section. -// -// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) +// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors // --------------------------------------------------------------------------- -// Uint64ArrayValue creates a typed array of uint64 values. -// -// value := Uint64ArrayValue([]uint64{1, 2, 3}) +// value := Uint64ArrayValue([]uint64{1, 2, 3}) func Uint64ArrayValue(values []uint64) Value { return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } -// Uint32ArrayValue creates a typed array of uint32 values. -// -// value := Uint32ArrayValue([]uint32{1, 2, 3}) +// value := Uint32ArrayValue([]uint32{1, 2, 3}) func Uint32ArrayValue(values []uint32) Value { return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } -// StringArrayValue creates a typed array of byte-string values. -// -// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) +// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) func StringArrayValue(values [][]byte) Value { return Value{Type: ArrayFlag | TypeString, stringArray: values} } -// ObjectArrayValue creates a typed array of Section values. -// -// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) +// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) func ObjectArrayValue(values []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: values} } @@ -176,7 +138,7 @@ func ObjectArrayValue(values []Section) Value { // Scalar accessors // --------------------------------------------------------------------------- -// AsUint64 returns the uint64 value or an error on type mismatch. +// value, err := Uint64Value(42).AsUint64() func (v Value) AsUint64() (uint64, error) { if v.Type != TypeUint64 { return 0, ErrorStorageTypeMismatch @@ -184,7 +146,7 @@ func (v Value) AsUint64() (uint64, error) { return v.uintVal, nil } -// AsUint32 returns the uint32 value or an error on type mismatch. +// value, err := Uint32Value(42).AsUint32() func (v Value) AsUint32() (uint32, error) { if v.Type != TypeUint32 { return 0, ErrorStorageTypeMismatch @@ -192,7 +154,7 @@ func (v Value) AsUint32() (uint32, error) { return uint32(v.uintVal), nil } -// AsUint16 returns the uint16 value or an error on type mismatch. +// value, err := Uint16Value(42).AsUint16() func (v Value) AsUint16() (uint16, error) { if v.Type != TypeUint16 { return 0, ErrorStorageTypeMismatch @@ -200,7 +162,7 @@ func (v Value) AsUint16() (uint16, error) { return uint16(v.uintVal), nil } -// AsUint8 returns the uint8 value or an error on type mismatch. +// value, err := Uint8Value(42).AsUint8() func (v Value) AsUint8() (uint8, error) { if v.Type != TypeUint8 { return 0, ErrorStorageTypeMismatch @@ -208,7 +170,7 @@ func (v Value) AsUint8() (uint8, error) { return uint8(v.uintVal), nil } -// AsInt64 returns the int64 value or an error on type mismatch. +// value, err := Int64Value(42).AsInt64() func (v Value) AsInt64() (int64, error) { if v.Type != TypeInt64 { return 0, ErrorStorageTypeMismatch @@ -216,7 +178,7 @@ func (v Value) AsInt64() (int64, error) { return v.intVal, nil } -// AsInt32 returns the int32 value or an error on type mismatch. +// value, err := Int32Value(42).AsInt32() func (v Value) AsInt32() (int32, error) { if v.Type != TypeInt32 { return 0, ErrorStorageTypeMismatch @@ -224,7 +186,7 @@ func (v Value) AsInt32() (int32, error) { return int32(v.intVal), nil } -// AsInt16 returns the int16 value or an error on type mismatch. +// value, err := Int16Value(42).AsInt16() func (v Value) AsInt16() (int16, error) { if v.Type != TypeInt16 { return 0, ErrorStorageTypeMismatch @@ -232,7 +194,7 @@ func (v Value) AsInt16() (int16, error) { return int16(v.intVal), nil } -// AsInt8 returns the int8 value or an error on type mismatch. +// value, err := Int8Value(42).AsInt8() func (v Value) AsInt8() (int8, error) { if v.Type != TypeInt8 { return 0, ErrorStorageTypeMismatch @@ -240,7 +202,7 @@ func (v Value) AsInt8() (int8, error) { return int8(v.intVal), nil } -// AsBool returns the bool value or an error on type mismatch. +// value, err := BoolValue(true).AsBool() func (v Value) AsBool() (bool, error) { if v.Type != TypeBool { return false, ErrorStorageTypeMismatch @@ -248,7 +210,7 @@ func (v Value) AsBool() (bool, error) { return v.boolVal, nil } -// AsDouble returns the float64 value or an error on type mismatch. +// value, err := DoubleValue(3.14).AsDouble() func (v Value) AsDouble() (float64, error) { if v.Type != TypeDouble { return 0, ErrorStorageTypeMismatch @@ -256,7 +218,7 @@ func (v Value) AsDouble() (float64, error) { return v.floatVal, nil } -// AsString returns the byte-string value or an error on type mismatch. +// value, err := StringValue([]byte("hello")).AsString() func (v Value) AsString() ([]byte, error) { if v.Type != TypeString { return nil, ErrorStorageTypeMismatch @@ -264,7 +226,7 @@ func (v Value) AsString() ([]byte, error) { return v.bytesVal, nil } -// AsSection returns the nested Section or an error on type mismatch. +// section, err := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}).AsSection() func (v Value) AsSection() (Section, error) { if v.Type != TypeObject { return nil, ErrorStorageTypeMismatch @@ -276,7 +238,7 @@ func (v Value) AsSection() (Section, error) { // Array accessors // --------------------------------------------------------------------------- -// AsUint64Array returns the []uint64 array or an error on type mismatch. +// values, err := Uint64ArrayValue([]uint64{1, 2, 3}).AsUint64Array() func (v Value) AsUint64Array() ([]uint64, error) { if v.Type != (ArrayFlag | TypeUint64) { return nil, ErrorStorageTypeMismatch @@ -284,7 +246,7 @@ func (v Value) AsUint64Array() ([]uint64, error) { return v.uint64Array, nil } -// AsUint32Array returns the []uint32 array or an error on type mismatch. +// values, err := Uint32ArrayValue([]uint32{1, 2, 3}).AsUint32Array() func (v Value) AsUint32Array() ([]uint32, error) { if v.Type != (ArrayFlag | TypeUint32) { return nil, ErrorStorageTypeMismatch @@ -292,7 +254,7 @@ func (v Value) AsUint32Array() ([]uint32, error) { return v.uint32Array, nil } -// AsStringArray returns the [][]byte array or an error on type mismatch. +// values, err := StringArrayValue([][]byte{[]byte("a"), []byte("b")}).AsStringArray() func (v Value) AsStringArray() ([][]byte, error) { if v.Type != (ArrayFlag | TypeString) { return nil, ErrorStorageTypeMismatch @@ -300,7 +262,7 @@ func (v Value) AsStringArray() ([][]byte, error) { return v.stringArray, nil } -// AsSectionArray returns the []Section array or an error on type mismatch. +// values, err := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}).AsSectionArray() func (v Value) AsSectionArray() ([]Section, error) { if v.Type != (ArrayFlag | TypeObject) { return nil, ErrorStorageTypeMismatch @@ -312,11 +274,7 @@ func (v Value) AsSectionArray() ([]Section, error) { // Encoder // --------------------------------------------------------------------------- -// EncodeStorage serialises a Section to the portable storage binary format, -// including the 9-byte header. Keys are sorted alphabetically to ensure -// deterministic output. -// -// data, err := EncodeStorage(section) +// data, err := EncodeStorage(section) func EncodeStorage(section Section) ([]byte, error) { buffer := make([]byte, 0, 256) @@ -486,10 +444,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { // Decoder // --------------------------------------------------------------------------- -// DecodeStorage deserialises portable storage binary data (including the -// 9-byte header) into a Section. -// -// section, err := DecodeStorage(data) +// section, err := DecodeStorage(data) func DecodeStorage(data []byte) (Section, error) { if len(data) < StorageHeaderSize { return nil, ErrorStorageTruncated diff --git a/node/levin/varint.go b/node/levin/varint.go index 2d26512..b245833 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,11 +28,7 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes value using the epee portable-storage varint scheme. -// The low two bits of the first byte indicate the total encoded width; -// the remaining bits carry the value in little-endian order. -// -// encoded := PackVarint(42) +// encoded := PackVarint(42) func PackVarint(value uint64) []byte { switch { case value <= varintMax1: @@ -55,10 +51,7 @@ func PackVarint(value uint64) []byte { } } -// UnpackVarint decodes one epee portable-storage varint from buffer. -// It returns the decoded value, the number of bytes consumed, and any error. -// -// value, err := UnpackVarint(buffer) +// value, err := UnpackVarint(buffer) func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated diff --git a/node/message.go b/node/message.go index 1640fde..eb21c41 100644 --- a/node/message.go +++ b/node/message.go @@ -22,14 +22,10 @@ const ( // versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} -// RawMessage stores an already-encoded JSON payload for deferred decoding. -// -// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) +// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) type RawMessage []byte -// MarshalJSON preserves the raw JSON payload when the message is encoded. -// -// data, err := RawMessage(`{"ok":true}`).MarshalJSON() +// data, err := RawMessage(`{"ok":true}`).MarshalJSON() func (m RawMessage) MarshalJSON() ([]byte, error) { if m == nil { return []byte("null"), nil @@ -38,10 +34,8 @@ func (m RawMessage) MarshalJSON() ([]byte, error) { return m, nil } -// UnmarshalJSON stores the raw JSON payload bytes without decoding them. -// -// var payload RawMessage -// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) +// var payload RawMessage +// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) func (m *RawMessage) UnmarshalJSON(data []byte) error { if m == nil { return core.E("node.RawMessage.UnmarshalJSON", "raw message target is nil", nil) @@ -51,9 +45,7 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error { return nil } -// IsProtocolVersionSupported checks if a given version is supported. -// -// ok := IsProtocolVersionSupported("1.0") +// ok := IsProtocolVersionSupported("1.0") func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } @@ -90,9 +82,7 @@ const ( MessageError MessageType = "error" ) -// Message represents a P2P message between nodes. -// -// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) +// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -103,9 +93,7 @@ type Message struct { ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to } -// NewMessage builds a message with a generated ID and timestamp. -// -// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) +// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) func NewMessage(messageType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -151,18 +139,14 @@ func (m *Message) ParsePayload(target any) error { // --- Payload Types --- -// HandshakePayload is sent during connection establishment. -// -// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} +// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} type HandshakePayload struct { Identity NodeIdentity `json:"identity"` Challenge []byte `json:"challenge,omitempty"` // Random bytes for auth Version string `json:"version"` // Protocol version } -// HandshakeAckPayload is the response to a handshake. -// -// ack := HandshakeAckPayload{Accepted: true} +// ack := HandshakeAckPayload{Accepted: true} type HandshakeAckPayload struct { Identity NodeIdentity `json:"identity"` ChallengeResponse []byte `json:"challengeResponse,omitempty"` @@ -170,49 +154,37 @@ type HandshakeAckPayload struct { Reason string `json:"reason,omitempty"` // If not accepted } -// PingPayload for keepalive/latency measurement. -// -// payload := PingPayload{SentAt: 42} +// payload := PingPayload{SentAt: 42} type PingPayload struct { SentAt int64 `json:"sentAt"` // Unix timestamp in milliseconds } -// PongPayload response to ping. -// -// payload := PongPayload{SentAt: 42, ReceivedAt: 43} +// payload := PongPayload{SentAt: 42, ReceivedAt: 43} type PongPayload struct { SentAt int64 `json:"sentAt"` // Echo of ping's sentAt ReceivedAt int64 `json:"receivedAt"` // When ping was received } -// StartMinerPayload requests starting a miner. -// -// payload := StartMinerPayload{MinerType: "xmrig"} +// payload := StartMinerPayload{MinerType: "xmrig"} type StartMinerPayload struct { MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") ProfileID string `json:"profileId,omitempty"` Config RawMessage `json:"config,omitempty"` // Override profile config } -// StopMinerPayload requests stopping a miner. -// -// payload := StopMinerPayload{MinerName: "xmrig-0"} +// payload := StopMinerPayload{MinerName: "xmrig-0"} type StopMinerPayload struct { MinerName string `json:"minerName"` } -// MinerAckPayload acknowledges a miner start/stop operation. -// -// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} +// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} type MinerAckPayload struct { Success bool `json:"success"` MinerName string `json:"minerName,omitempty"` Error string `json:"error,omitempty"` } -// MinerStatsItem represents stats for a single miner. -// -// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} +// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} type MinerStatsItem struct { Name string `json:"name"` Type string `json:"type"` @@ -225,9 +197,7 @@ type MinerStatsItem struct { CPUThreads int `json:"cpuThreads,omitempty"` } -// StatsPayload contains miner statistics. -// -// stats := StatsPayload{NodeID: "worker-1"} +// stats := StatsPayload{NodeID: "worker-1"} type StatsPayload struct { NodeID string `json:"nodeId"` NodeName string `json:"nodeName"` @@ -235,27 +205,21 @@ type StatsPayload struct { Uptime int64 `json:"uptime"` // Node uptime in seconds } -// LogsRequestPayload requests console logs from a miner. -// -// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} +// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} type LogsRequestPayload struct { MinerName string `json:"minerName"` Lines int `json:"lines"` // Number of lines to fetch Since int64 `json:"since,omitempty"` // Unix timestamp, logs after this time } -// LogsPayload contains console log lines. -// -// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} +// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} type LogsPayload struct { MinerName string `json:"minerName"` Lines []string `json:"lines"` HasMore bool `json:"hasMore"` // More logs available } -// DeployPayload contains a deployment bundle. -// -// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} +// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} type DeployPayload struct { BundleType string `json:"type"` // "profile" | "miner" | "full" Data []byte `json:"data"` // STIM-encrypted bundle @@ -263,18 +227,14 @@ type DeployPayload struct { Name string `json:"name"` // Profile or miner name } -// DeployAckPayload acknowledges a deployment. -// -// ack := DeployAckPayload{Success: true, Name: "xmrig"} +// ack := DeployAckPayload{Success: true, Name: "xmrig"} type DeployAckPayload struct { Success bool `json:"success"` Name string `json:"name,omitempty"` Error string `json:"error,omitempty"` } -// ErrorPayload contains error information. -// -// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} +// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} type ErrorPayload struct { Code int `json:"code"` Message string `json:"message"` diff --git a/node/peer.go b/node/peer.go index b44348a..42f8801 100644 --- a/node/peer.go +++ b/node/peer.go @@ -15,16 +15,14 @@ import ( "github.com/adrg/xdg" ) -// Peer represents a known remote node. -// // peer := &Peer{ -// ID: "worker-1", -// Name: "Worker 1", -// Address: "127.0.0.1:9101", -// PingMilliseconds: 42.5, -// GeographicKilometres: 100, -// Score: 80, -// } +// ID: "worker-1", +// Name: "Worker 1", +// Address: "127.0.0.1:9101", +// PingMilliseconds: 42.5, +// GeographicKilometres: 100, +// Score: 80, +// } type Peer struct { ID string `json:"id"` Name string `json:"name"` @@ -47,9 +45,7 @@ type Peer struct { // peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second -// PeerAuthMode controls how unknown peers are handled -// -// mode := PeerAuthAllowlist +// mode := PeerAuthAllowlist type PeerAuthMode int const ( @@ -125,9 +121,7 @@ var ( scoreWeight = 1.2 ) -// NewPeerRegistry loads the default peer registry. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() func NewPeerRegistry() (*PeerRegistry, error) { peersPath, err := xdg.ConfigFile("lethean-desktop/peers.json") if err != nil { @@ -137,12 +131,10 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// NewPeerRegistryFromPath loads or creates a peer registry at an explicit path. -// // Missing files are treated as an empty registry; malformed registry files are // returned as errors so callers can repair the persisted state. // -// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -165,9 +157,7 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } -// SetAuthMode changes how unknown peers are handled. -// -// registry.SetAuthMode(PeerAuthAllowlist) +// registry.SetAuthMode(PeerAuthAllowlist) func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -175,18 +165,14 @@ func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { logging.Info("peer auth mode changed", logging.Fields{"mode": mode}) } -// GetAuthMode returns the current authentication mode. -// -// mode := registry.GetAuthMode() +// mode := registry.GetAuthMode() func (r *PeerRegistry) GetAuthMode() PeerAuthMode { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.authMode } -// AllowPublicKey adds a public key to the allowlist. -// -// registry.AllowPublicKey(peer.PublicKey) +// registry.AllowPublicKey(peer.PublicKey) func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -194,9 +180,7 @@ func (r *PeerRegistry) AllowPublicKey(publicKey string) { logging.Debug("public key added to allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// RevokePublicKey removes a public key from the allowlist. -// -// registry.RevokePublicKey(peer.PublicKey) +// registry.RevokePublicKey(peer.PublicKey) func (r *PeerRegistry) RevokePublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -204,20 +188,17 @@ func (r *PeerRegistry) RevokePublicKey(publicKey string) { logging.Debug("public key removed from allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// IsPublicKeyAllowed checks if a public key is in the allowlist. -// -// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) +// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.allowedPublicKeys[publicKey] } -// IsPeerAllowed checks if a peer is allowed to connect based on auth mode. // Returns true when AuthMode is Open (all allowed), or when Allowlist mode is active // and the peer is pre-registered or its public key is in the allowlist. // -// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) +// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { r.allowedPublicKeyMu.RLock() authMode := r.authMode @@ -242,18 +223,14 @@ func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { return keyAllowed } -// ListAllowedPublicKeys returns all allowlisted public keys. -// -// keys := registry.ListAllowedPublicKeys() +// keys := registry.ListAllowedPublicKeys() func (r *PeerRegistry) ListAllowedPublicKeys() []string { return slices.Collect(r.AllowedPublicKeys()) } -// AllowedPublicKeys returns an iterator over all allowlisted public keys. -// // for key := range registry.AllowedPublicKeys() { -// log.Printf("allowed: %s", key[:16]) -// } +// log.Printf("allowed: %s", key[:16]) +// } func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { return func(yield func(string) bool) { r.allowedPublicKeyMu.RLock() @@ -267,10 +244,9 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// AddPeer adds a new peer to the registry. // Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. // -// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) +// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { r.mu.Lock() @@ -306,10 +282,9 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { return nil } -// UpdatePeer updates an existing peer's information. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) +// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { r.mu.Lock() @@ -326,10 +301,9 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { return nil } -// RemovePeer removes a peer from the registry. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.RemovePeer("worker-1") +// err := registry.RemovePeer("worker-1") func (r *PeerRegistry) RemovePeer(id string) error { r.mu.Lock() @@ -346,9 +320,7 @@ func (r *PeerRegistry) RemovePeer(id string) error { return nil } -// GetPeer returns a copy of the peer with the supplied ID. -// -// peer := registry.GetPeer("worker-1") +// peer := registry.GetPeer("worker-1") func (r *PeerRegistry) GetPeer(id string) *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -363,13 +335,16 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return &peerCopy } -// ListPeers returns all registered peers. +// peers := registry.ListPeers() func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) } -// Peers returns an iterator over all registered peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.Peers() { +// _ = peer +// } func (r *PeerRegistry) Peers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -384,10 +359,7 @@ func (r *PeerRegistry) Peers() iter.Seq[*Peer] { } } -// UpdateMetrics updates a peer's performance metrics. -// -// registry.UpdateMetrics("worker-1", 42.5, 100, 3) -// +// registry.UpdateMetrics("worker-1", 42.5, 100, 3) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilometres float64, hopCount int) error { r.mu.Lock() @@ -410,7 +382,7 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilo return nil } -// UpdateScore updates a peer's reliability score. +// registry.UpdateScore("worker-1", 90) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateScore(id string, score float64) error { r.mu.Lock() @@ -432,9 +404,7 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { return nil } -// SetConnected updates a peer's connection state. -// -// registry.SetConnected("worker-1", true) +// registry.SetConnected("worker-1", true) func (r *PeerRegistry) SetConnected(id string, connected bool) { r.mu.Lock() defer r.mu.Unlock() @@ -457,9 +427,7 @@ const ( ScoreDefault = 50.0 // Default score for new peers ) -// RecordSuccess records a successful interaction with a peer, improving their score. -// -// registry.RecordSuccess("worker-1") +// registry.RecordSuccess("worker-1") func (r *PeerRegistry) RecordSuccess(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -474,9 +442,7 @@ func (r *PeerRegistry) RecordSuccess(id string) { r.scheduleSave() } -// RecordFailure records a failed interaction with a peer, reducing their score. -// -// registry.RecordFailure("worker-1") +// registry.RecordFailure("worker-1") func (r *PeerRegistry) RecordFailure(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -497,9 +463,7 @@ func (r *PeerRegistry) RecordFailure(id string) { }) } -// RecordTimeout records a timeout when communicating with a peer. -// -// registry.RecordTimeout("worker-1") +// registry.RecordTimeout("worker-1") func (r *PeerRegistry) RecordTimeout(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -520,9 +484,7 @@ func (r *PeerRegistry) RecordTimeout(id string) { }) } -// GetPeersByScore returns peers sorted by score, highest first. -// -// peers := registry.GetPeersByScore() +// peers := registry.GetPeersByScore() func (r *PeerRegistry) GetPeersByScore() []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -543,11 +505,9 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return peers } -// PeersByScore returns an iterator over peers sorted by score (highest first). -// // for peer := range registry.PeersByScore() { -// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) -// } +// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) +// } func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { peers := r.GetPeersByScore() @@ -559,10 +519,9 @@ func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { } } -// SelectOptimalPeer returns the best peer based on multi-factor optimisation. // Uses Poindexter KD-tree to find the peer closest to ideal metrics (low ping, low hops, high score). // -// peer := registry.SelectOptimalPeer() +// peer := registry.SelectOptimalPeer() func (r *PeerRegistry) SelectOptimalPeer() *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -589,9 +548,7 @@ func (r *PeerRegistry) SelectOptimalPeer() *Peer { return &peerCopy } -// SelectNearestPeers returns the n best peers based on multi-factor optimisation. -// -// peers := registry.SelectNearestPeers(3) +// peers := registry.SelectNearestPeers(3) func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -616,15 +573,16 @@ func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { return peers } -// GetConnectedPeers returns all currently connected peers as a slice. -// -// connectedPeers := registry.GetConnectedPeers() +// connectedPeers := registry.GetConnectedPeers() func (r *PeerRegistry) GetConnectedPeers() []*Peer { return slices.Collect(r.ConnectedPeers()) } -// ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.ConnectedPeers() { +// _ = peer +// } func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -641,9 +599,7 @@ func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { } } -// Count returns the number of registered peers. -// -// n := registry.Count() +// n := registry.Count() func (r *PeerRegistry) Count() int { r.mu.RLock() defer r.mu.RUnlock() @@ -751,7 +707,7 @@ func (r *PeerRegistry) saveNow() error { return nil } -// Close flushes any pending changes and releases resources. +// registry.Close() func (r *PeerRegistry) Close() error { // Cancel any pending timer and save immediately if changes are queued. r.saveMutex.Lock() diff --git a/node/protocol.go b/node/protocol.go index 72c1624..23d5152 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -4,9 +4,7 @@ import ( core "dappco.re/go/core" ) -// ProtocolError represents an error from the remote peer. -// -// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} +// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} type ProtocolError struct { Code int Message string @@ -16,14 +14,10 @@ func (e *ProtocolError) Error() string { return core.Sprintf("remote error (%d): %s", e.Code, e.Message) } -// ResponseHandler provides helpers for handling protocol responses. -// -// handler := &ResponseHandler{} +// handler := &ResponseHandler{} type ResponseHandler struct{} -// ValidateResponse checks a response against the expected type. -// -// err := handler.ValidateResponse(resp, MessageStats) +// err := handler.ValidateResponse(resp, MessageStats) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { return core.E("ResponseHandler.ValidateResponse", "nil response", nil) @@ -46,9 +40,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy return nil } -// ParseResponse validates the response and parses the payload into the target. -// -// err := handler.ParseResponse(resp, MessageStats, &stats) +// err := handler.ParseResponse(resp, MessageStats, &stats) func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error { if err := h.ValidateResponse(resp, expectedType); err != nil { return err @@ -80,17 +72,13 @@ func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } -// IsProtocolError returns true if the error is a ProtocolError. -// -// ok := IsProtocolError(err) +// ok := IsProtocolError(err) func IsProtocolError(err error) bool { _, ok := err.(*ProtocolError) return ok } -// GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. -// -// code := GetProtocolErrorCode(err) +// code := GetProtocolErrorCode(err) func GetProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code diff --git a/node/transport.go b/node/transport.go index f580073..e2c74e9 100644 --- a/node/transport.go +++ b/node/transport.go @@ -39,9 +39,7 @@ const ( defaultTransportMaximumConnections = 100 ) -// TransportConfig configures the WebSocket transport. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -53,9 +51,7 @@ type TransportConfig struct { PongTimeout time.Duration // Timeout waiting for pong } -// DefaultTransportConfig returns sensible defaults. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -92,23 +88,17 @@ func (c TransportConfig) maximumConnections() int { return defaultTransportMaximumConnections } -// MessageHandler processes incoming messages. -// -// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} +// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} type MessageHandler func(peerConnection *PeerConnection, message *Message) -// MessageDeduplicator tracks recent message IDs to prevent duplicate processing. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) type MessageDeduplicator struct { recentMessageTimes map[string]time.Time mutex sync.RWMutex timeToLive time.Duration } -// NewMessageDeduplicator creates a deduplicator with the supplied retention window. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator { d := &MessageDeduplicator{ recentMessageTimes: make(map[string]time.Time), @@ -170,10 +160,7 @@ type PeerRateLimiter struct { mutex sync.Mutex } -// NewPeerRateLimiter creates a token bucket seeded with maxTokens and refilled -// at refillRate tokens per second. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { return &PeerRateLimiter{ availableTokens: maxTokens, @@ -979,9 +966,7 @@ func (pc *PeerConnection) Close() error { return err } -// DisconnectPayload contains reason for disconnect. -// -// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} +// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} type DisconnectPayload struct { Reason string `json:"reason"` Code int `json:"code"` // Optional disconnect code -- 2.45.3 From 1ee54add395bd882c8a0c3fe7745f65952d7bb2f Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:11:17 +0000 Subject: [PATCH 44/60] refactor(node): align remaining AX naming and examples Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/routing.md | 2 +- docs/ueps.md | 2 +- node/dispatcher.go | 11 +++++------ node/dispatcher_test.go | 12 ++++++------ node/integration_test.go | 2 +- node/levin/header.go | 12 ++++++------ node/message.go | 24 +++++++++--------------- node/protocol.go | 8 ++------ node/transport.go | 20 ++++++++++---------- node/worker.go | 6 ++---- 11 files changed, 44 insertions(+), 57 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 1f9f3a2..bb4b90c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -179,7 +179,7 @@ Auto-connect: if the target peer is not yet connected, `sendRequest` calls `tran |----------|-------|---------| | `IntentHandshake` | `0x01` | Connection establishment | | `IntentCompute` | `0x20` | Compute job request | -| `IntentRehab` | `0x30` | Benevolent intervention (pause execution) | +| `IntentPauseExecution` | `0x30` | Benevolent intervention (pause execution) | | `IntentCustom` | `0xFF` | Application-level sub-protocols | **Sentinel errors**: diff --git a/docs/routing.md b/docs/routing.md index 2bb3405..3881f01 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -84,7 +84,7 @@ Dropped packets are logged at WARN level with the threat score, threshold, inten const ( IntentHandshake byte = 0x01 // Connection establishment / hello IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) + IntentPauseExecution byte = 0x30 IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) ``` diff --git a/docs/ueps.md b/docs/ueps.md index 09c0b71..24ab498 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -156,7 +156,7 @@ Reserved intent values: |----|----------|---------| | `0x01` | `IntentHandshake` | Connection establishment / hello | | `0x20` | `IntentCompute` | Compute job request | -| `0x30` | `IntentRehab` | Benevolent intervention (pause execution) | +| `0x30` | `IntentPauseExecution` | Benevolent intervention (pause execution) | | `0xFF` | `IntentCustom` | Extended / application-level sub-protocols | ## Threat Score diff --git a/node/dispatcher.go b/node/dispatcher.go index faedf4b..53e5cff 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -13,13 +13,12 @@ import ( // threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 -// Well-known intent identifiers. These correspond to the semantic tokens -// carried in the UEPS IntentID header field (RFC-021). +// intentID := IntentPauseExecution const ( - IntentHandshake byte = 0x01 // Connection establishment / hello - IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) - IntentCustom byte = 0xFF // Extended / application-level sub-protocols + IntentHandshake byte = 0x01 + IntentCompute byte = 0x20 + IntentPauseExecution byte = 0x30 + IntentCustom byte = 0xFF ) // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } diff --git a/node/dispatcher_test.go b/node/dispatcher_test.go index 1cbe6e3..d458875 100644 --- a/node/dispatcher_test.go +++ b/node/dispatcher_test.go @@ -136,7 +136,7 @@ func TestDispatcher_UnknownIntentDropped_Bad(t *testing.T) { func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { d := NewDispatcher() - var handshakeCalled, computeCalled, rehabCalled, customCalled bool + var handshakeCalled, computeCalled, pauseExecutionCalled, customCalled bool d.RegisterHandler(IntentHandshake, func(pkt *ueps.ParsedPacket) error { handshakeCalled = true @@ -146,8 +146,8 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { computeCalled = true return nil }) - d.RegisterHandler(IntentRehab, func(pkt *ueps.ParsedPacket) error { - rehabCalled = true + d.RegisterHandler(IntentPauseExecution, func(pkt *ueps.ParsedPacket) error { + pauseExecutionCalled = true return nil }) d.RegisterHandler(IntentCustom, func(pkt *ueps.ParsedPacket) error { @@ -162,7 +162,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { }{ {"handshake routes correctly", IntentHandshake, &handshakeCalled}, {"compute routes correctly", IntentCompute, &computeCalled}, - {"rehab routes correctly", IntentRehab, &rehabCalled}, + {"pause execution routes correctly", IntentPauseExecution, &pauseExecutionCalled}, {"custom routes correctly", IntentCustom, &customCalled}, } @@ -171,7 +171,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { // Reset all flags handshakeCalled = false computeCalled = false - rehabCalled = false + pauseExecutionCalled = false customCalled = false pkt := makePacket(tt.intentID, 0, []byte("payload")) @@ -341,6 +341,6 @@ func TestDispatcher_IntentConstants_Good(t *testing.T) { // Verify the well-known intent IDs match the spec (RFC-021). assert.Equal(t, byte(0x01), IntentHandshake) assert.Equal(t, byte(0x20), IntentCompute) - assert.Equal(t, byte(0x30), IntentRehab) + assert.Equal(t, byte(0x30), IntentPauseExecution) assert.Equal(t, byte(0xFF), IntentCustom) } diff --git a/node/integration_test.go b/node/integration_test.go index bd45880..13d40bc 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -572,7 +572,7 @@ func TestIntegration_DispatcherWithRealUEPSPackets_Good(t *testing.T) { }{ {IntentHandshake, "handshake", "hello"}, {IntentCompute, "compute", `{"job":"123"}`}, - {IntentRehab, "rehab", "pause"}, + {IntentPauseExecution, "pause-execution", "pause"}, {IntentCustom, "custom", "app-specific-data"}, } diff --git a/node/levin/header.go b/node/levin/header.go index 911435a..9da0760 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -11,25 +11,25 @@ import ( core "dappco.re/go/core" ) -// HeaderSize is the exact byte length of a serialised Levin header. +// headerBytes := make([]byte, HeaderSize) const HeaderSize = 33 -// Signature is the magic value that opens every Levin packet. +// header.Signature = Signature const Signature uint64 = 0x0101010101012101 -// MaxPayloadSize is the upper bound we accept for a single payload (100 MB). +// header.PayloadSize <= MaxPayloadSize const MaxPayloadSize uint64 = 100 * 1024 * 1024 -// Return-code constants carried in every Levin response. const ( + // returnCode := ReturnOK ReturnOK int32 = 0 ReturnErrorConnection int32 = -1 ReturnErrorFormat int32 = -7 ReturnErrorSignature int32 = -13 ) -// Command IDs for the CryptoNote P2P layer. const ( + // commandID := CommandHandshake CommandHandshake uint32 = 1001 CommandTimedSync uint32 = 1002 CommandPing uint32 = 1003 @@ -41,8 +41,8 @@ const ( CommandResponseChain uint32 = 2007 ) -// Sentinel errors returned by DecodeHeader. var ( + // err := ErrorBadSignature ErrorBadSignature = core.E("levin", "bad signature", nil) ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) diff --git a/node/message.go b/node/message.go index eb21c41..7fc85e4 100644 --- a/node/message.go +++ b/node/message.go @@ -8,18 +8,14 @@ import ( "github.com/google/uuid" ) -// Protocol version constants. const ( - // ProtocolVersion is the current protocol version. + // version := ProtocolVersion ProtocolVersion = "1.0" - // MinProtocolVersion is the minimum supported version. + // minimumVersion := MinProtocolVersion MinProtocolVersion = "1.0" ) -// SupportedProtocolVersions lists all protocol versions this node supports. -// Used for version negotiation during handshake. -// -// versions := SupportedProtocolVersions +// versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} // payload := RawMessage(`{"pool":"pool.example.com:3333"}`) @@ -50,9 +46,7 @@ func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } -// MessageType defines the type of P2P message. -// -// messageType := MessagePing +// messageType := MessagePing type MessageType string const ( @@ -241,12 +235,12 @@ type ErrorPayload struct { Details string `json:"details,omitempty"` } -// Common error codes. const ( - ErrorCodeUnknown = 1000 - ErrorCodeInvalidMessage = 1001 - ErrorCodeUnauthorized = 1002 - ErrorCodeNotFound = 1003 + ErrorCodeUnknown = 1000 + ErrorCodeInvalidMessage = 1001 + ErrorCodeUnauthorized = 1002 + ErrorCodeNotFound = 1003 + // code := ErrorCodeOperationFailed ErrorCodeOperationFailed = 1004 ErrorCodeTimeout = 1005 ) diff --git a/node/protocol.go b/node/protocol.go index 23d5152..0565e76 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -58,16 +58,12 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, // handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} -// ValidateResponse is a convenience function using the default handler. -// -// err := ValidateResponse(message, MessageStats) +// err := ValidateResponse(message, MessageStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } -// ParseResponse is a convenience function using the default handler. -// -// err := ParseResponse(message, MessageStats, &stats) +// err := ParseResponse(message, MessageStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } diff --git a/node/transport.go b/node/transport.go index e2c74e9..f537787 100644 --- a/node/transport.go +++ b/node/transport.go @@ -27,10 +27,10 @@ var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). const messageLogSampleInterval = 100 -// DefaultMaxMessageSize is the default maximum message size (1MB) +// limit := DefaultMaxMessageSize const DefaultMaxMessageSize int64 = 1 << 20 // 1MB -// agentUserAgentPrefix identifies this tool in request headers. +// prefix := agentUserAgentPrefix const agentUserAgentPrefix = "agent-go-p2p" const ( @@ -41,14 +41,14 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // ":9091" default - WebSocketPath string // "/ws" - WebSocket endpoint path - TLSCertPath string // Optional TLS for wss:// - TLSKeyPath string - MaxConnections int // Maximum concurrent connections - MaxMessageSize int64 // Maximum message size in bytes (0 = 1MB default) - PingInterval time.Duration // WebSocket keepalive interval - PongTimeout time.Duration // Timeout waiting for pong + ListenAddr string // config.ListenAddr = ":9091" + WebSocketPath string // config.WebSocketPath = "/ws" + TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" + TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" + MaxConnections int // config.MaxConnections = 100 + MaxMessageSize int64 // config.MaxMessageSize = 1 << 20 + PingInterval time.Duration // config.PingInterval = 30 * time.Second + PongTimeout time.Duration // config.PongTimeout = 10 * time.Second } // transportConfig := DefaultTransportConfig() diff --git a/node/worker.go b/node/worker.go index 3de7277..073cdfc 100644 --- a/node/worker.go +++ b/node/worker.go @@ -39,7 +39,7 @@ type Worker struct { minerManager MinerManager profileManager ProfileManager startedAt time.Time - DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) + DeploymentDirectory string // worker.DeploymentDirectory = "/srv/p2p/deployments" } // worker := NewWorker(nodeManager, transport) @@ -395,9 +395,7 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// RegisterOnTransport installs the worker message handler on the transport. -// -// worker.RegisterOnTransport() +// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -- 2.45.3 From e5953e4b861caa4f5a88aea8fd3e092f82fdb18d Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:19:03 +0000 Subject: [PATCH 45/60] refactor(node): align AX comments and peer copy semantics Co-Authored-By: Virgil --- docs/architecture.md | 10 ++++----- docs/discovery.md | 8 +++---- docs/history.md | 6 ++--- docs/identity.md | 14 ++++++------ docs/transport.md | 12 +++++----- node/bundle.go | 8 ++----- node/identity.go | 5 ++--- node/peer.go | 33 +++++++++++++++++++++++----- node/peer_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ node/transport.go | 33 +++++++++++++++++++++++----- node/transport_test.go | 11 ++++++++++ 11 files changed, 146 insertions(+), 44 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index bb4b90c..8771504 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. -**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMu` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. +**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMutex` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. **Buffer pool**: `MarshalJSON` uses a `sync.Pool` of `bytes.Buffer` (initial capacity 1 KB, maximum pooled size 64 KB) to reduce allocation pressure in the message serialisation hot path. HTML escaping is disabled to match `json.Marshal` semantics. @@ -70,15 +70,15 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Peer fields persisted**: - `ID`, `Name`, `PublicKey`, `Address`, `Role`, `AddedAt`, `LastSeen` -- `PingMS`, `Hops`, `GeoKM`, `Score` (float64, 0–100) +- `PingMilliseconds`, `Hops`, `GeographicKilometres`, `Score` (float64, 0–100) **KD-tree dimensions** (lower is better in all axes): | Dimension | Weight | Rationale | |-----------|--------|-----------| -| `PingMS` | 1.0 | Latency dominates interactive performance | +| `PingMilliseconds` | 1.0 | Latency dominates interactive performance | | `Hops` | 0.7 | Network hop count (routing cost) | -| `GeoKM` | 0.2 | Geographic distance (minor factor) | +| `GeographicKilometres` | 0.2 | Geographic distance (minor factor) | | `100 - Score` | 1.2 | Reliability (inverted so lower = better peer) | `SelectOptimalPeer()` queries the tree for the point nearest to the origin (ideal: zero latency, zero hops, zero distance, maximum score). `SelectNearestPeers(n)` returns the n best. @@ -236,7 +236,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. |----------|------------| | `Transport.conns` | `sync.RWMutex` | | `Transport.handler` | `sync.RWMutex` | -| `PeerConnection` writes | `sync.Mutex` (`writeMu`) | +| `PeerConnection` writes | `sync.Mutex` (`writeMutex`) | | `PeerConnection` close | `sync.Once` (`closeOnce`) | | `PeerRegistry.peers` + KD-tree | `sync.RWMutex` | | `PeerRegistry.allowedPublicKeys` | separate `sync.RWMutex` | diff --git a/docs/discovery.md b/docs/discovery.md index 3423165..452bb94 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -20,9 +20,9 @@ type Peer struct { LastSeen time.Time `json:"lastSeen"` // Poindexter metrics (updated dynamically) - PingMS float64 `json:"pingMs"` // Latency in milliseconds + PingMilliseconds float64 `json:"pingMs"` // Latency in milliseconds Hops int `json:"hops"` // Network hop count - GeoKM float64 `json:"geoKm"` // Geographic distance in kilometres + GeographicKilometres float64 `json:"geoKm"` // Geographic distance in kilometres Score float64 `json:"score"` // Reliability score 0--100 Connected bool `json:"-"` // Not persisted @@ -83,9 +83,9 @@ The registry maintains a 4-dimensional KD-tree for optimal peer selection. Each | Dimension | Source | Weight | Direction | |-----------|--------|--------|-----------| -| Latency | `PingMS` | 1.0 | Lower is better | +| Latency | `PingMilliseconds` | 1.0 | Lower is better | | Hops | `Hops` | 0.7 | Lower is better | -| Geographic distance | `GeoKM` | 0.2 | Lower is better | +| Geographic distance | `GeographicKilometres` | 0.2 | Lower is better | | Reliability | `100 - Score` | 1.2 | Inverted so lower is better | The score dimension is inverted so that the "ideal peer" target point `[0, 0, 0, 0]` represents zero latency, zero hops, zero distance, and maximum reliability (score 100). diff --git a/docs/history.md b/docs/history.md index 5c42f56..5c42f07 100644 --- a/docs/history.md +++ b/docs/history.md @@ -31,7 +31,7 @@ Tests covered: - MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close -- Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) +- Concurrent sends: no data races under `go test -race` (`writeMutex` protects all writes) ### Phase 3 — Controller Tests @@ -104,9 +104,9 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMu`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. -Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMu`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. +Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. ## Wiki Corrections (19 February 2026) diff --git a/docs/identity.md b/docs/identity.md index b102683..05c1a58 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -39,13 +39,13 @@ Paths follow XDG base directories via `github.com/adrg/xdg`. The private key is ### Creating an Identity ```go -nm, err := node.NewNodeManager() +nodeManager, err := node.NewNodeManager() if err != nil { log.Fatal(err) } // Generate a new identity (persists key and config to disk) -err = nm.GenerateIdentity("eu-controller-01", node.RoleController) +err = nodeManager.GenerateIdentity("eu-controller-01", node.RoleController) ``` Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce the X25519 keypair. @@ -53,7 +53,7 @@ Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce ### Custom Paths (Testing) ```go -nm, err := node.NewNodeManagerFromPaths( +nodeManager, err := node.NewNodeManagerFromPaths( "/tmp/test/private.key", "/tmp/test/node.json", ) @@ -62,8 +62,8 @@ nm, err := node.NewNodeManagerFromPaths( ### Checking and Retrieving Identity ```go -if nm.HasIdentity() { - identity := nm.GetIdentity() // Returns a copy +if nodeManager.HasIdentity() { + identity := nodeManager.GetIdentity() // Returns a copy fmt.Println(identity.ID, identity.Name) } ``` @@ -73,7 +73,7 @@ if nm.HasIdentity() { ### Deriving Shared Secrets ```go -sharedSecret, err := nm.DeriveSharedSecret(peerPublicKeyBase64) +sharedSecret, err := nodeManager.DeriveSharedSecret(peerPublicKeyBase64) ``` This performs X25519 ECDH with the peer's public key and hashes the result with SHA-256, producing a 32-byte symmetric key. The same shared secret is derived independently by both sides (no secret is transmitted). @@ -81,7 +81,7 @@ This performs X25519 ECDH with the peer's public key and hashes the result with ### Deleting an Identity ```go -err := nm.Delete() // Removes key and config from disk, clears in-memory state +err := nodeManager.Delete() // Removes key and config from disk, clears in-memory state ``` ## Challenge-Response Authentication diff --git a/docs/transport.md b/docs/transport.md index 5aad542..07a25e6 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -25,7 +25,7 @@ type TransportConfig struct { Sensible defaults via `DefaultTransportConfig()`: ```go -cfg := node.DefaultTransportConfig() +transportConfig := node.DefaultTransportConfig() // ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -33,10 +33,10 @@ cfg := node.DefaultTransportConfig() ## Creating and Starting ```go -transport := node.NewTransport(nodeManager, peerRegistry, cfg) +transport := node.NewTransport(nodeManager, peerRegistry, transportConfig) // Set message handler before Start() to avoid races -transport.OnMessage(func(conn *node.PeerConnection, msg *node.Message) { +transport.OnMessage(func(peerConnection *node.PeerConnection, msg *node.Message) { // Handle incoming messages }) @@ -96,15 +96,15 @@ type PeerConnection struct { ### Sending Messages ```go -err := peerConn.Send(msg) +err := peerConnection.Send(msg) ``` -`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMu` mutex serialises concurrent writes. +`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMutex` serialises concurrent writes. ### Graceful Close ```go -err := peerConn.GracefulClose("shutting down", node.DisconnectShutdown) +err := peerConnection.GracefulClose("shutting down", node.DisconnectShutdown) ``` Sends a `disconnect` message (best-effort) before closing the connection. Uses `sync.Once` to ensure the connection is only closed once. diff --git a/node/bundle.go b/node/bundle.go index f1c7342..30e409b 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -347,9 +347,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return firstExecutable, nil } -// StreamBundle writes a bundle to a writer (for large transfers). -// -// err := StreamBundle(bundle, writer) +// err := StreamBundle(bundle, writer) func StreamBundle(bundle *Bundle, w io.Writer) error { result := core.JSONMarshal(bundle) if !result.OK { @@ -359,9 +357,7 @@ func StreamBundle(bundle *Bundle, w io.Writer) error { return err } -// ReadBundle reads a bundle from a reader. -// -// bundle, err := ReadBundle(reader) +// bundle, err := ReadBundle(reader) func ReadBundle(r io.Reader) (*Bundle, error) { var buf bytes.Buffer if _, err := io.Copy(&buf, r); err != nil { diff --git a/node/identity.go b/node/identity.go index ae5d1f1..7bb3a1d 100644 --- a/node/identity.go +++ b/node/identity.go @@ -87,10 +87,9 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// Missing files are treated as a fresh install; malformed or partial identity -// state is returned as an error so callers can handle it explicitly. -// // nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// Missing files are treated as a fresh install; malformed or partial identity +// state returns an error so callers can handle it explicitly. func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, diff --git a/node/peer.go b/node/peer.go index 42f8801..27a80d7 100644 --- a/node/peer.go +++ b/node/peer.go @@ -131,10 +131,9 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// Missing files are treated as an empty registry; malformed registry files are -// returned as errors so callers can repair the persisted state. -// // peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// Missing files are treated as an empty registry; malformed registry files +// return an error so callers can repair the persisted state. func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -248,6 +247,13 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { // // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.AddPeer", "peer is nil", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if peer.ID == "" { @@ -271,7 +277,7 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { peer.AddedAt = time.Now() } if peer.Score == 0 { - peer.Score = 50 // Default neutral score + peer.Score = ScoreDefault } r.peers[peer.ID] = peer @@ -286,6 +292,17 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { // // err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.UpdatePeer", "peer is nil", nil) + } + + if peer.ID == "" { + return core.E("PeerRegistry.UpdatePeer", "peer ID is required", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if _, exists := r.peers[peer.ID]; !exists { @@ -502,7 +519,13 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return 0 }) - return peers + peerCopies := make([]*Peer, 0, len(peers)) + for _, peer := range peers { + peerCopy := *peer + peerCopies = append(peerCopies, &peerCopy) + } + + return peerCopies } // for peer := range registry.PeersByScore() { diff --git a/node/peer_test.go b/node/peer_test.go index b10979c..45d6f0f 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -65,6 +65,15 @@ func TestPeer_Registry_AddPeer_Good(t *testing.T) { t.Errorf("expected 1 peer, got %d", pr.Count()) } + peer.Name = "Mutated after add" + stored := pr.GetPeer("test-peer-1") + if stored == nil { + t.Fatal("expected peer to exist after add") + } + if stored.Name != "Test Peer" { + t.Errorf("expected stored peer to remain unchanged, got %q", stored.Name) + } + // Try to add duplicate err = pr.AddPeer(peer) if err == nil { @@ -634,6 +643,34 @@ func TestPeer_Registry_PeersSortedByScore_Good(t *testing.T) { if sorted[2].ID != "low-score" { t.Errorf("third peer should be low-score, got %s", sorted[2].ID) } + + sorted[0].Name = "Mutated" + restored := pr.GetPeer("high-score") + if restored == nil { + t.Fatal("expected high-score peer to still exist") + } + if restored.Name != "High" { + t.Errorf("expected registry peer to remain unchanged, got %q", restored.Name) + } +} + +func TestPeer_Registry_NilPeerInputs_Bad(t *testing.T) { + pr, cleanup := setupTestPeerRegistry(t) + defer cleanup() + + t.Run("AddPeer", func(t *testing.T) { + err := pr.AddPeer(nil) + if err == nil { + t.Fatal("expected error when adding nil peer") + } + }) + + t.Run("UpdatePeer", func(t *testing.T) { + err := pr.UpdatePeer(nil) + if err == nil { + t.Fatal("expected error when updating nil peer") + } + }) } // --- Additional coverage tests for peer.go --- @@ -736,6 +773,19 @@ func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { if updated.Score != 80 { t.Errorf("expected score 80, got %f", updated.Score) } + + peer.Name = "Mutated after update" + peer.Score = 12 + stored := pr.GetPeer("update-test") + if stored == nil { + t.Fatal("expected peer to exist after update mutation") + } + if stored.Name != "Updated" { + t.Errorf("expected stored peer name to remain Updated, got %q", stored.Name) + } + if stored.Score != 80 { + t.Errorf("expected stored peer score to remain 80, got %f", stored.Score) + } } func TestPeer_Registry_UpdateMetrics_NotFound_Bad(t *testing.T) { diff --git a/node/transport.go b/node/transport.go index f537787..9c93165 100644 --- a/node/transport.go +++ b/node/transport.go @@ -107,22 +107,45 @@ func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator return d } -// IsDuplicate checks whether a message ID is still within the retention window. +// duplicate := deduplicator.IsDuplicate(message.ID) func (d *MessageDeduplicator) IsDuplicate(msgID string) bool { d.mutex.RLock() - _, exists := d.recentMessageTimes[msgID] + seenAt, exists := d.recentMessageTimes[msgID] + retentionWindow := d.timeToLive d.mutex.RUnlock() - return exists + + if !exists { + return false + } + + if retentionWindow > 0 && time.Since(seenAt) <= retentionWindow { + return true + } + + d.mutex.Lock() + defer d.mutex.Unlock() + + seenAt, exists = d.recentMessageTimes[msgID] + if !exists { + return false + } + + if retentionWindow <= 0 || time.Since(seenAt) > retentionWindow { + delete(d.recentMessageTimes, msgID) + return false + } + + return true } -// Mark records a message ID as recently seen. +// deduplicator.Mark(message.ID) func (d *MessageDeduplicator) Mark(msgID string) { d.mutex.Lock() d.recentMessageTimes[msgID] = time.Now() d.mutex.Unlock() } -// Cleanup removes expired entries from the deduplicator. +// deduplicator.Cleanup() func (d *MessageDeduplicator) Cleanup() { d.mutex.Lock() defer d.mutex.Unlock() diff --git a/node/transport_test.go b/node/transport_test.go index 24969b1..16acd46 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -153,6 +153,17 @@ func TestTransport_MessageDeduplicator_Good(t *testing.T) { } }) + t.Run("ExpiredEntriesDoNotLinger", func(t *testing.T) { + d := NewMessageDeduplicator(50 * time.Millisecond) + d.Mark("msg-1") + + time.Sleep(75 * time.Millisecond) + + if d.IsDuplicate("msg-1") { + t.Error("should not be duplicate after TTL even before cleanup runs") + } + }) + t.Run("ConcurrentAccess", func(t *testing.T) { d := NewMessageDeduplicator(5 * time.Minute) var wg sync.WaitGroup -- 2.45.3 From 849a716360b344b4d977e9e4e955388c2b3ed5d8 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:25:37 +0000 Subject: [PATCH 46/60] refactor(node): trim redundant AX comments Co-Authored-By: Virgil --- node/buffer_pool.go | 2 -- node/bundle.go | 20 -------------------- node/controller.go | 12 ------------ node/errors.go | 2 -- node/identity.go | 9 --------- node/levin/connection.go | 2 -- node/peer.go | 6 ------ node/worker.go | 23 ----------------------- 8 files changed, 76 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index 91f160d..b883e09 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -15,14 +15,12 @@ var bufferPool = sync.Pool{ }, } -// getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { buffer := bufferPool.Get().(*bytes.Buffer) buffer.Reset() return buffer } -// putBuffer returns a buffer to the pool. func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) if buffer.Cap() <= 65536 { diff --git a/node/bundle.go b/node/bundle.go index 30e409b..034fd95 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -46,20 +46,17 @@ type BundleManifest struct { // bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { - // Create a TIM with just the profile config timBundle, err := tim.New() if err != nil { return nil, core.E("CreateProfileBundle", "failed to create TIM", err) } timBundle.Config = profileJSON - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateProfileBundle", "failed to encrypt bundle", err) } - // Calculate checksum checksum := calculateChecksum(stimData) return &Bundle{ @@ -84,14 +81,12 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e // bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { - // Read miner binary minerContent, err := filesystemRead(minerPath) if err != nil { return nil, core.E("CreateMinerBundle", "failed to read miner binary", err) } minerData := []byte(minerContent) - // Create a tarball with the miner binary tarData, err := createTarball(map[string][]byte{ core.PathBase(minerPath): minerData, }) @@ -99,24 +94,20 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo return nil, core.E("CreateMinerBundle", "failed to create tarball", err) } - // Create DataNode from tarball dataNode, err := datanode.FromTar(tarData) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create datanode", err) } - // Create TIM from DataNode timBundle, err := tim.FromDataNode(dataNode) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create TIM", err) } - // Set profile as config if provided if profileJSON != nil { timBundle.Config = profileJSON } - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateMinerBundle", "failed to encrypt bundle", err) @@ -134,17 +125,14 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo // profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { - // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { return nil, core.E("ExtractProfileBundle", "checksum mismatch - bundle may be corrupted", nil) } - // If it's unencrypted JSON, just return it if isJSON(bundle.Data) { return bundle.Data, nil } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return nil, core.E("ExtractProfileBundle", "failed to decrypt bundle", err) @@ -155,24 +143,20 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { - // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { return "", nil, core.E("ExtractMinerBundle", "checksum mismatch - bundle may be corrupted", nil) } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to decrypt bundle", err) } - // Convert rootfs to tarball and extract tarData, err := timBundle.RootFS.ToTar() if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) } - // Extract tarball to destination minerPath, err := extractTarball(tarData, destDir) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to extract tarball", err) @@ -186,13 +170,11 @@ func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } -// calculateChecksum computes SHA-256 checksum of data. func calculateChecksum(data []byte) string { hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } -// isJSON checks if data starts with JSON characters. func isJSON(data []byte) bool { if len(data) == 0 { return false @@ -201,7 +183,6 @@ func isJSON(data []byte) bool { return data[0] == '{' || data[0] == '[' } -// createTarball creates a tar archive from a map of filename -> content. func createTarball(files map[string][]byte) ([]byte, error) { var buf bytes.Buffer tarWriter := tar.NewWriter(&buf) @@ -248,7 +229,6 @@ func createTarball(files map[string][]byte) ([]byte, error) { return buf.Bytes(), nil } -// extractTarball extracts a tar archive to a directory, returns first executable found. func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir := destDir diff --git a/node/controller.go b/node/controller.go index 17bbc22..8441352 100644 --- a/node/controller.go +++ b/node/controller.go @@ -36,7 +36,6 @@ func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transpo return c } -// handleResponse processes incoming replies and routes them to the waiting request. func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { if message.ReplyTo == "" { return // Not a response, let worker handle it @@ -58,12 +57,6 @@ func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { } } -// sendRequest registers a temporary response channel, sends message, and waits -// for the matching reply or timeout. -// -// The response channel is intentionally never closed. Removing it from the -// pending map is enough to stop future routing, and it avoids a late-response -// close/send race after the caller has already timed out. func (c *Controller) sendRequest(peerID string, message *Message, timeout time.Duration) (*Message, error) { resolvedPeerID := peerID @@ -77,13 +70,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D if err != nil { return nil, core.E("Controller.sendRequest", "failed to connect to peer", err) } - // Use the real peer ID after handshake (it may have changed) resolvedPeerID = conn.Peer.ID - // Update the message destination message.To = resolvedPeerID } - // Create response channel responseChannel := make(chan *Message, 1) c.mutex.Lock() @@ -98,12 +88,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D c.mutex.Unlock() }() - // Send the message if err := c.transport.Send(resolvedPeerID, message); err != nil { return nil, core.E("Controller.sendRequest", "failed to send message", err) } - // Wait for response ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() diff --git a/node/errors.go b/node/errors.go index 302a34a..26ac3d6 100644 --- a/node/errors.go +++ b/node/errors.go @@ -3,9 +3,7 @@ package node import core "dappco.re/go/core" var ( - // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) diff --git a/node/identity.go b/node/identity.go index 7bb3a1d..9679305 100644 --- a/node/identity.go +++ b/node/identity.go @@ -199,15 +199,12 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error return hash[:], nil } -// savePrivateKey saves the private key to disk with restricted permissions. func (n *NodeManager) savePrivateKey() error { - // Ensure directory exists dir := core.PathDir(n.keyPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } - // Write private key if err := filesystemWrite(n.keyPath, string(n.privateKey)); err != nil { return core.E("NodeManager.savePrivateKey", "failed to write private key", err) } @@ -215,9 +212,7 @@ func (n *NodeManager) savePrivateKey() error { return nil } -// saveIdentity saves the public identity to the config file. func (n *NodeManager) saveIdentity() error { - // Ensure directory exists dir := core.PathDir(n.configPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.saveIdentity", "failed to create config directory", err) @@ -236,9 +231,7 @@ func (n *NodeManager) saveIdentity() error { return nil } -// loadIdentity loads the node identity from disk. func (n *NodeManager) loadIdentity() error { - // Load identity config content, err := filesystemRead(n.configPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read identity", err) @@ -250,14 +243,12 @@ func (n *NodeManager) loadIdentity() error { return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", result.Value.(error)) } - // Load private key keyContent, err := filesystemRead(n.keyPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read private key", err) } privateKey := []byte(keyContent) - // Reconstruct keypair from private key keyPair, err := stmf.LoadKeyPair(privateKey) if err != nil { return core.E("NodeManager.loadIdentity", "failed to load keypair", err) diff --git a/node/levin/connection.go b/node/levin/connection.go index 76bd643..a479b04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -79,7 +79,6 @@ func (connection *Connection) WriteResponse(commandID uint32, payload []byte, re return connection.writeFrame(&header, payload) } -// writeFrame serialises header + payload and writes them atomically. func (connection *Connection) writeFrame(header *Header, payload []byte) error { headerBytes := EncodeHeader(header) @@ -109,7 +108,6 @@ func (connection *Connection) ReadPacket() (Header, []byte, error) { return Header{}, nil, err } - // Read header. var headerBytes [HeaderSize]byte if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err diff --git a/node/peer.go b/node/peer.go index 27a80d7..55e9fa6 100644 --- a/node/peer.go +++ b/node/peer.go @@ -64,7 +64,6 @@ const ( // peerNamePattern validates peer names: alphanumeric, hyphens, underscores, and spaces. var peerNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-_ ]{0,62}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`) -// safeKeyPrefix returns a truncated key for logging, handling short keys safely func safeKeyPrefix(key string) string { if len(key) >= 16 { return key[:16] + "..." @@ -75,9 +74,6 @@ func safeKeyPrefix(key string) string { return key } -// validatePeerName checks if a peer name is valid. -// Peer names must be 1-64 characters, start and end with alphanumeric, -// and contain only alphanumeric, hyphens, underscores, and spaces. func validatePeerName(name string) error { if name == "" { return nil // Empty names are allowed (optional field) @@ -347,7 +343,6 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return nil } - // Return a copy peerCopy := *peer return &peerCopy } @@ -508,7 +503,6 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { peers := slices.Collect(maps.Values(r.peers)) - // Sort by score descending slices.SortFunc(peers, func(a, b *Peer) int { if b.Score > a.Score { return 1 diff --git a/node/worker.go b/node/worker.go index 073cdfc..03875f2 100644 --- a/node/worker.go +++ b/node/worker.go @@ -111,7 +111,6 @@ func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) } } -// handlePing responds to ping requests. func (w *Worker) handlePing(message *Message) (*Message, error) { var ping PingPayload if err := message.ParsePayload(&ping); err != nil { @@ -126,7 +125,6 @@ func (w *Worker) handlePing(message *Message) (*Message, error) { return message.Reply(MessagePong, pong) } -// handleStats responds with current miner statistics. func (w *Worker) handleStats(message *Message) (*Message, error) { identity := w.nodeManager.GetIdentity() if identity == nil { @@ -148,8 +146,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { continue } - // Convert to MinerStatsItem - this is a simplified conversion - // The actual implementation would need to match the mining package's stats structure item := convertMinerStats(miner, minerStats) stats.Miners = append(stats.Miners, item) } @@ -158,7 +154,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { return message.Reply(MessageStats, stats) } -// convertMinerStats converts a running miner's raw stats map to the wire protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ Name: miner.GetName(), @@ -189,7 +184,6 @@ func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { return item } -// handleStartMiner starts a miner with the given profile. func (w *Worker) handleStartMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -200,12 +194,10 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "invalid start miner payload", err) } - // Validate miner type is provided if payload.MinerType == "" { return nil, core.E("Worker.handleStartMiner", "miner type is required", nil) } - // Get the config from the profile or use the override var config any if payload.Config != nil { config = payload.Config @@ -219,7 +211,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "no config provided and no profile manager configured", nil) } - // Start the miner miner, err := w.minerManager.StartMiner(payload.MinerType, config) if err != nil { ack := MinerAckPayload{ @@ -236,7 +227,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleStopMiner stops a running miner. func (w *Worker) handleStopMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -259,7 +249,6 @@ func (w *Worker) handleStopMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleLogs returns console logs from a miner. func (w *Worker) handleLogs(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -270,7 +259,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return nil, core.E("Worker.handleLogs", "invalid logs payload", err) } - // Validate and limit the Lines parameter to prevent resource exhaustion const maxLogLines = 10000 if payload.Lines <= 0 || payload.Lines > maxLogLines { payload.Lines = maxLogLines @@ -292,7 +280,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return message.Reply(MessageLogs, logs) } -// handleDeploy handles deployment of profiles or miner bundles. func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) (*Message, error) { var payload DeployPayload if err := message.ParsePayload(&payload); err != nil { @@ -319,13 +306,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return nil, core.E("Worker.handleDeploy", "profile manager not configured", nil) } - // Decrypt and extract profile data profileData, err := ExtractProfileBundle(bundle, password) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract profile bundle", err) } - // Unmarshal into interface{} to pass to ProfileManager var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", result.Value.(error)) @@ -347,8 +332,6 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return message.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: - // Determine the installation directory under the configured deployment - // root for lethean-desktop/miners/. minersDir := core.JoinPath(w.deploymentDirectory(), "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) @@ -358,13 +341,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) "type": payload.BundleType, }) - // Extract miner bundle minerPath, profileData, err := ExtractMinerBundle(bundle, password, installDir) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract miner bundle", err) } - // If the bundle contained a profile config, save it if len(profileData) > 0 && w.profileManager != nil { var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { @@ -376,13 +357,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } - // Success response ack := DeployAckPayload{ Success: true, Name: payload.Name, } - // Log the installation logging.Info("miner bundle installed successfully", logging.Fields{ "name": payload.Name, "miner_path": minerPath, @@ -395,12 +374,10 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -// deploymentDirectory resolves the active deployment directory. func (w *Worker) deploymentDirectory() string { if w.DeploymentDirectory != "" { return w.DeploymentDirectory -- 2.45.3 From 2eecb6cfc695dabf68e4f8ff77f574045eb872d4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:32:55 +0000 Subject: [PATCH 47/60] refactor(node): align transport naming with AX Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/history.md | 2 +- docs/transport.md | 8 +-- node/peer.go | 6 -- node/transport.go | 133 +++++++++++++++++++++++++---------------- node/transport_test.go | 4 +- specs/node.md | 6 +- 7 files changed, 93 insertions(+), 68 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8771504..dc204a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| -| `ListenAddr` | `:9091` | HTTP bind address | +| `ListenAddress` | `:9091` | HTTP bind address | | `WebSocketPath` | `/ws` | WebSocket endpoint | | `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | diff --git a/docs/history.md b/docs/history.md index 5c42f07..986727f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -104,7 +104,7 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.WebSocketConnection.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. diff --git a/docs/transport.md b/docs/transport.md index 07a25e6..56b55f9 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -11,7 +11,7 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { - ListenAddr string // ":9091" default + ListenAddress string // ":9091" default WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go transportConfig := node.DefaultTransportConfig() -// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 +// ListenAddress: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -86,8 +86,8 @@ Each active connection is wrapped in a `PeerConnection`: ```go type PeerConnection struct { - Peer *Peer // Remote peer identity - Conn *websocket.Conn // Underlying WebSocket + Peer *Peer // Remote peer identity + WebSocketConnection *websocket.Conn // Underlying WebSocket SharedSecret []byte // From X25519 ECDH LastActivity time.Time } diff --git a/node/peer.go b/node/peer.go index 55e9fa6..2b7606b 100644 --- a/node/peer.go +++ b/node/peer.go @@ -42,7 +42,6 @@ type Peer struct { Connected bool `json:"-"` } -// peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second // mode := PeerAuthAllowlist @@ -239,8 +238,6 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. -// // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { if peer == nil { @@ -623,8 +620,6 @@ func (r *PeerRegistry) Count() int { return len(r.peers) } -// rebuildKDTree rebuilds the KD-tree from current peers. -// Must be called with lock held. func (r *PeerRegistry) rebuildKDTree() { if len(r.peers) == 0 { r.kdTree = nil @@ -746,7 +741,6 @@ func (r *PeerRegistry) Close() error { return nil } -// load reads peers from disk. func (r *PeerRegistry) load() error { content, err := filesystemRead(r.path) if err != nil { diff --git a/node/transport.go b/node/transport.go index 9c93165..6004aed 100644 --- a/node/transport.go +++ b/node/transport.go @@ -21,7 +21,6 @@ import ( "github.com/gorilla/websocket" ) -// messageLogSampleCounter tracks message counts for sampled debug logs. var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). @@ -41,7 +40,8 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // config.ListenAddr = ":9091" + ListenAddress string // config.ListenAddress = ":9091" + ListenAddr string WebSocketPath string // config.WebSocketPath = "/ws" TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" @@ -54,6 +54,7 @@ type TransportConfig struct { // transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ + ListenAddress: defaultTransportListenAddress, ListenAddr: defaultTransportListenAddress, WebSocketPath: defaultTransportWebSocketPath, MaxConnections: defaultTransportMaximumConnections, @@ -63,16 +64,22 @@ func DefaultTransportConfig() TransportConfig { } } -// listenAddress returns the effective listen address, falling back to the -// default when the config leaves it empty. func (c TransportConfig) listenAddress() string { + if c.ListenAddress != "" && c.ListenAddress != defaultTransportListenAddress { + return c.ListenAddress + } + if c.ListenAddr != "" && c.ListenAddr != defaultTransportListenAddress { + return c.ListenAddr + } + if c.ListenAddress != "" { + return c.ListenAddress + } if c.ListenAddr != "" { return c.ListenAddr } return defaultTransportListenAddress } -// webSocketPath returns the effective WebSocket endpoint path. func (c TransportConfig) webSocketPath() string { if c.WebSocketPath != "" { return c.WebSocketPath @@ -80,7 +87,6 @@ func (c TransportConfig) webSocketPath() string { return defaultTransportWebSocketPath } -// maximumConnections returns the effective concurrent connection limit. func (c TransportConfig) maximumConnections() int { if c.MaxConnections > 0 { return c.MaxConnections @@ -217,15 +223,16 @@ func (r *PeerRateLimiter) Allow() bool { // peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { - Peer *Peer - Conn *websocket.Conn - SharedSecret []byte // Derived via X25519 ECDH, used for SMSG - LastActivity time.Time - UserAgent string // Request identity advertised by the peer - writeMutex sync.Mutex // Serialize WebSocket writes - transport *Transport - closeOnce sync.Once // Ensure Close() is only called once - rateLimiter *PeerRateLimiter // Per-peer message rate limiting + Peer *Peer + WebSocketConnection *websocket.Conn + Conn *websocket.Conn + SharedSecret []byte // Derived via X25519 ECDH, used for SMSG + LastActivity time.Time + UserAgent string // Request identity advertised by the peer + writeMutex sync.Mutex // Serialize WebSocket writes + transport *Transport + closeOnce sync.Once // Ensure Close() is only called once + rateLimiter *PeerRateLimiter // Per-peer message rate limiting } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) @@ -261,7 +268,13 @@ func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config T } } -// agentHeaderToken converts free-form identity data into a stable header token. +func (pc *PeerConnection) webSocketConnection() *websocket.Conn { + if pc.WebSocketConnection != nil { + return pc.WebSocketConnection + } + return pc.Conn +} + func agentHeaderToken(value string) string { value = strings.TrimSpace(value) if value == "" { @@ -295,7 +308,6 @@ func agentHeaderToken(value string) string { return token } -// agentUserAgent returns a transparent identity string for request headers. func (t *Transport) agentUserAgent() string { identity := t.nodeManager.GetIdentity() if identity == nil { @@ -437,12 +449,13 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } peerConnection := &PeerConnection{ - Peer: peer, - Conn: conn, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } // Perform handshake with challenge-response authentication. @@ -531,7 +544,6 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { return t.connections[peerID] } -// handleWebSocketUpgrade handles incoming WebSocket connections. func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -668,13 +680,14 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques } pc := &PeerConnection{ - Peer: peer, - Conn: conn, - SharedSecret: sharedSecret, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + SharedSecret: sharedSecret, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } identity := t.nodeManager.GetIdentity() @@ -738,16 +751,20 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques go t.keepalive(pc) } -// performHandshake initiates handshake with a peer. func (t *Transport) performHandshake(pc *PeerConnection) error { // Set handshake timeout handshakeTimeout := 10 * time.Second - pc.Conn.SetWriteDeadline(time.Now().Add(handshakeTimeout)) - pc.Conn.SetReadDeadline(time.Now().Add(handshakeTimeout)) + connection := pc.webSocketConnection() + if connection == nil { + return core.E("Transport.performHandshake", "websocket connection is nil", nil) + } + + connection.SetWriteDeadline(time.Now().Add(handshakeTimeout)) + connection.SetReadDeadline(time.Now().Add(handshakeTimeout)) defer func() { // Reset deadlines after handshake - pc.Conn.SetWriteDeadline(time.Time{}) - pc.Conn.SetReadDeadline(time.Time{}) + connection.SetWriteDeadline(time.Time{}) + connection.SetReadDeadline(time.Time{}) }() identity := t.nodeManager.GetIdentity() @@ -778,12 +795,12 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return core.E("Transport.performHandshake", "marshal handshake message", err) } - if err := pc.Conn.WriteMessage(websocket.TextMessage, data); err != nil { + if err := connection.WriteMessage(websocket.TextMessage, data); err != nil { return core.E("Transport.performHandshake", "send handshake", err) } // Wait for ack - _, ackData, err := pc.Conn.ReadMessage() + _, ackData, err := connection.ReadMessage() if err != nil { return core.E("Transport.performHandshake", "read handshake ack", err) } @@ -844,7 +861,6 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return nil } -// readLoop reads messages from a peer connection. func (t *Transport) readLoop(pc *PeerConnection) { defer t.waitGroup.Done() defer t.removeConnection(pc) @@ -854,7 +870,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { if maxSize <= 0 { maxSize = DefaultMaxMessageSize } - pc.Conn.SetReadLimit(maxSize) + connection := pc.webSocketConnection() + if connection == nil { + return + } + + connection.SetReadLimit(maxSize) for { select { @@ -865,12 +886,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { // Set read deadline to prevent blocking forever on unresponsive connections readDeadline := t.config.PingInterval + t.config.PongTimeout - if err := pc.Conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { + if err := connection.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { logging.Error("SetReadDeadline error", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return } - _, data, err := pc.Conn.ReadMessage() + _, data, err := connection.ReadMessage() if err != nil { logging.Debug("read error from peer", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return @@ -913,7 +934,6 @@ func (t *Transport) readLoop(pc *PeerConnection) { } } -// keepalive sends periodic pings. func (t *Transport) keepalive(pc *PeerConnection) { defer t.waitGroup.Done() @@ -948,7 +968,6 @@ func (t *Transport) keepalive(pc *PeerConnection) { } } -// removeConnection removes and cleans up a connection. func (t *Transport) removeConnection(pc *PeerConnection) { t.mutex.Lock() delete(t.connections, pc.Peer.ID) @@ -972,19 +991,28 @@ func (pc *PeerConnection) sendLocked(msg *Message) error { return err } - if err := pc.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + connection := pc.webSocketConnection() + if connection == nil { + return core.E("PeerConnection.Send", "websocket connection is nil", nil) + } + + if err := connection.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { return core.E("PeerConnection.Send", "failed to set write deadline", err) } - defer pc.Conn.SetWriteDeadline(time.Time{}) + defer connection.SetWriteDeadline(time.Time{}) - return pc.Conn.WriteMessage(websocket.BinaryMessage, data) + return connection.WriteMessage(websocket.BinaryMessage, data) } // err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { - err = pc.Conn.Close() + connection := pc.webSocketConnection() + if connection == nil { + return + } + err = connection.Close() }) return err } @@ -1011,6 +1039,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + connection := pc.webSocketConnection() + if connection == nil { + return + } + // Try to send disconnect message (best effort). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() @@ -1027,12 +1060,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } // Close the underlying connection - err = pc.Conn.Close() + err = connection.Close() }) return err } -// encryptMessage encrypts a message using SMSG with the shared secret. func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, error) { // Serialize message to JSON (using pooled buffer for efficiency) msgData, err := MarshalJSON(msg) @@ -1053,7 +1085,6 @@ func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, e return encrypted, nil } -// decryptMessage decrypts a message using SMSG with the shared secret. func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, error) { // Decrypt using shared secret as password password := base64.StdEncoding.EncodeToString(sharedSecret) diff --git a/node/transport_test.go b/node/transport_test.go index 16acd46..5d00ab7 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -562,7 +562,7 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { if clientConn == nil { t.Fatal("client should have connection to server") } - clientConn.Conn.Close() + clientConn.WebSocketConnection.Close() // Wait for server to detect and clean up deadline := time.After(2 * time.Second) @@ -779,7 +779,7 @@ func TestTransport_StartAndStop_Good(t *testing.T) { nm := newTestNodeManager(t, "start-test", RoleWorker) reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() - cfg.ListenAddr = ":0" // Let OS pick a free port + cfg.ListenAddress = ":0" // Let OS pick a free port tr := NewTransport(nm, reg, cfg) diff --git a/specs/node.md b/specs/node.md index 99f6eb8..d00f942 100644 --- a/specs/node.md +++ b/specs/node.md @@ -25,14 +25,14 @@ | `NodeRole` | `type NodeRole string` | Operational mode string for controller, worker, or dual-role nodes. | | `Peer` | `struct{ ID string; Name string; PublicKey string; Address string; Role NodeRole; AddedAt time.Time; LastSeen time.Time; PingMS float64; Hops int; GeoKM float64; Score float64; Connected bool }` | Registry record for a remote node, including addressing, role, scoring metrics, and transient connection state. | | `PeerAuthMode` | `type PeerAuthMode int` | Peer admission policy used by `PeerRegistry` when unknown peers attempt to connect. | -| `PeerConnection` | `struct{ Peer *Peer; Conn *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | +| `PeerConnection` | `struct{ Peer *Peer; WebSocketConnection *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | | `PeerRateLimiter` | `struct{ /* unexported fields */ }` | Per-peer token bucket limiter used by the transport hot path. | | `PeerRegistry` | `struct{ /* unexported fields */ }` | Concurrent peer store with KD-tree selection, allowlist state, and debounced persistence to disk. | | `ProtocolError` | `struct{ Code int; Message string }` | Structured remote error returned by protocol response helpers when a peer replies with `MsgError`. | | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddress string; ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -88,7 +88,7 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddress=:9091`, `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | -- 2.45.3 From cd7535fc801acc976334008b8db89e9f87835e5a Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:20:17 +0000 Subject: [PATCH 48/60] refactor(logging): prefer get-style logger accessors Co-Authored-By: Virgil --- logging/compat.go | 20 ++++++++++---------- logging/logger.go | 28 ++++++++++++++-------------- logging/logger_test.go | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/logging/compat.go b/logging/compat.go index 404910c..74f014b 100644 --- a/logging/compat.go +++ b/logging/compat.go @@ -10,18 +10,18 @@ func (l *Logger) WithComponent(component string) *Logger { return l.ComponentLogger(component) } -// GetLevel returns the current log level. -// Preferred over Level for AX naming consistency. +// Level returns the current log level. +// Deprecated: Use GetLevel instead. // -// level := logger.GetLevel() -func (l *Logger) GetLevel() Level { - return l.Level() +// level := logger.Level() +func (l *Logger) Level() Level { + return l.GetLevel() } -// GetGlobal returns the global logger instance. -// Deprecated: Use Global instead. +// Global returns the global logger instance. +// Deprecated: Use GetGlobal instead. // -// logger := GetGlobal() -func GetGlobal() *Logger { - return Global() +// logger := Global() +func Global() *Logger { + return GetGlobal() } diff --git a/logging/logger.go b/logging/logger.go index a24c227..2746881 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -107,10 +107,10 @@ func (l *Logger) SetLevel(level Level) { l.level = level } -// Level returns the current log level. +// GetLevel returns the current log level. // -// level := logger.Level() -func (l *Logger) Level() Level { +// level := logger.GetLevel() +func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level @@ -242,10 +242,10 @@ func SetGlobal(l *Logger) { globalLogger = l } -// Global returns the global logger instance. +// GetGlobal returns the global logger instance. // -// logger := Global() -func Global() *Logger { +// logger := GetGlobal() +func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger @@ -266,56 +266,56 @@ func SetGlobalLevel(level Level) { // // Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { - Global().Debug(message, fields...) + GetGlobal().Debug(message, fields...) } // Info logs an informational message using the global logger. // // Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { - Global().Info(message, fields...) + GetGlobal().Info(message, fields...) } // Warn logs a warning message using the global logger. // // Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { - Global().Warn(message, fields...) + GetGlobal().Warn(message, fields...) } // Error logs an error message using the global logger. // // Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { - Global().Error(message, fields...) + GetGlobal().Error(message, fields...) } // Debugf logs a formatted debug message using the global logger. // // Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { - Global().Debugf(format, args...) + GetGlobal().Debugf(format, args...) } // Infof logs a formatted informational message using the global logger. // // Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { - Global().Infof(format, args...) + GetGlobal().Infof(format, args...) } // Warnf logs a formatted warning message using the global logger. // // Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { - Global().Warnf(format, args...) + GetGlobal().Warnf(format, args...) } // Errorf logs a formatted error message using the global logger. // // Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { - Global().Errorf(format, args...) + GetGlobal().Errorf(format, args...) } // ParseLevel parses a string into a log level. diff --git a/logging/logger_test.go b/logging/logger_test.go index 08c48a9..b46aaec 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -149,8 +149,8 @@ func TestLogger_SetLevel_Good(t *testing.T) { } // Verify Level - if logger.Level() != LevelInfo { - t.Error("Level should return LevelInfo") + if logger.GetLevel() != LevelInfo { + t.Error("GetLevel should return LevelInfo") } } -- 2.45.3 From 9a302ce7b5cd6c54f3d50661db5b1edb2f9cf6e4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:26:00 +0000 Subject: [PATCH 49/60] refactor(node): remove AX compatibility aliases Co-Authored-By: Virgil --- docs/architecture.md | 8 ++-- docs/development.md | 2 +- docs/history.md | 4 +- docs/transport.md | 12 +++--- docs/ueps.md | 4 +- logging/compat.go | 27 ------------ logging/compat_test.go | 39 ------------------ node/compat.go | 46 --------------------- node/compat_test.go | 57 ------------------------- node/levin/compat.go | 87 --------------------------------------- node/levin/compat_test.go | 66 ----------------------------- specs/logging.md | 5 +-- specs/node-levin.md | 34 +++++++-------- specs/node.md | 11 ++--- ueps/compat.go | 11 ----- ueps/compat_test.go | 14 ------- 16 files changed, 38 insertions(+), 389 deletions(-) delete mode 100644 logging/compat.go delete mode 100644 logging/compat_test.go delete mode 100644 node/compat.go delete mode 100644 node/compat_test.go delete mode 100644 node/levin/compat.go delete mode 100644 node/levin/compat_test.go delete mode 100644 ueps/compat.go delete mode 100644 ueps/compat_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 105c0ec..1f9f3a2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,8 +37,8 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| | `ListenAddr` | `:9091` | HTTP bind address | -| `WSPath` | `/ws` | WebSocket endpoint | -| `MaxConns` | 100 | Maximum concurrent connections | +| `WebSocketPath` | `/ws` | WebSocket endpoint | +| `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | | `PingInterval` | 30 s | Keepalive ping period | | `PongTimeout` | 10 s | Maximum time to wait for pong | @@ -56,7 +56,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Rate limiting**: Each `PeerConnection` holds a `PeerRateLimiter` (token bucket: 100 burst, 50 tokens/second refill). Messages from rate-limited peers are dropped in the read loop. -**MaxConns enforcement**: The handler tracks `pendingConns` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. +**MaxConnections enforcement**: The handler tracks `pendingHandshakeCount` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. @@ -244,7 +244,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. | `Controller.pending` | `sync.RWMutex` | | `MessageDeduplicator.seen` | `sync.RWMutex` | | `Dispatcher.handlers` | `sync.RWMutex` | -| `Transport.pendingConns` | `atomic.Int32` | +| `Transport.pendingHandshakeCount` | `atomic.Int32` | The codebase is verified race-free under `go test -race`. diff --git a/docs/development.md b/docs/development.md index 8d2bd3b..196c370 100644 --- a/docs/development.md +++ b/docs/development.md @@ -233,7 +233,7 @@ Examples: ``` feat(dispatcher): implement UEPS threat circuit breaker -test(transport): add keepalive timeout and MaxConns enforcement tests +test(transport): add keepalive timeout and MaxConnections enforcement tests fix(peer): prevent data race in GracefulClose (P2P-RACE-1) ``` diff --git a/docs/history.md b/docs/history.md index 52ea3f2..5c42f56 100644 --- a/docs/history.md +++ b/docs/history.md @@ -28,7 +28,7 @@ Tests covered: - Encrypted message round-trip: SMSG encrypt on one side, decrypt on other - Message deduplication: duplicate UUID dropped silently - Rate limiting: burst of more than 100 messages, subsequent drops after token bucket empties -- MaxConns enforcement: 503 HTTP rejection when limit is reached +- MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close - Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) @@ -92,7 +92,7 @@ The `TagPayload` (0xFF) field now uses the same 2-byte length prefix as the othe ### No Resource Cleanup on Some Error Paths -`transport.handleWSUpgrade` does not clean up on handshake timeout (the `pendingConns` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). +`transport.handleWebSocketUpgrade` does not clean up on handshake timeout (the `pendingHandshakeCount` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). These are low-severity gaps. They do not cause goroutine leaks under the current implementation because the connection's read loop is not started until after a successful handshake. diff --git a/docs/transport.md b/docs/transport.md index 19a7987..5aad542 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -12,10 +12,10 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { ListenAddr string // ":9091" default - WSPath string // "/ws" -- WebSocket endpoint path + WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string - MaxConns int // Maximum concurrent connections (default 100) + MaxConnections int // Maximum concurrent connections (default 100) MaxMessageSize int64 // Maximum message size in bytes (default 1MB) PingInterval time.Duration // Keepalive interval (default 30s) PongTimeout time.Duration // Pong wait timeout (default 10s) @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go cfg := node.DefaultTransportConfig() -// ListenAddr: ":9091", WSPath: "/ws", MaxConns: 100 +// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -123,9 +123,9 @@ const ( ## Incoming Connections -The transport exposes an HTTP handler at the configured `WSPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. +The transport exposes an HTTP handler at the configured `WebSocketPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. -The `MaxConns` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. +The `MaxConnections` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. ## Message Deduplication @@ -166,7 +166,7 @@ err = transport.Send(peerID, msg) err = transport.Broadcast(msg) // Query connections -count := transport.ConnectedPeers() +count := transport.ConnectedPeerCount() conn := transport.GetConnection(peerID) // Iterate over all connections diff --git a/docs/ueps.md b/docs/ueps.md index 2ba184a..09c0b71 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -25,8 +25,8 @@ Each field is encoded as a 1-byte tag, 2-byte big-endian length (uint16), and va | Tag | Constant | Value Size | Description | |-----|----------|------------|-------------| | `0x01` | `TagVersion` | 1 byte | Protocol version (default `0x09` for IPv9) | -| `0x02` | `TagCurrentLay` | 1 byte | Current network layer | -| `0x03` | `TagTargetLay` | 1 byte | Target network layer | +| `0x02` | `TagCurrentLayer` | 1 byte | Current network layer | +| `0x03` | `TagTargetLayer` | 1 byte | Target network layer | | `0x04` | `TagIntent` | 1 byte | Semantic intent token (routes the packet) | | `0x05` | `TagThreatScore` | 2 bytes | Threat score (0--65535, big-endian uint16) | | `0x06` | `TagHMAC` | 32 bytes | HMAC-SHA256 signature | diff --git a/logging/compat.go b/logging/compat.go deleted file mode 100644 index 74f014b..0000000 --- a/logging/compat.go +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package logging - -// WithComponent returns a new Logger scoped to one component. -// Deprecated: Use ComponentLogger instead. -// -// transportLogger := logger.WithComponent("transport") -func (l *Logger) WithComponent(component string) *Logger { - return l.ComponentLogger(component) -} - -// Level returns the current log level. -// Deprecated: Use GetLevel instead. -// -// level := logger.Level() -func (l *Logger) Level() Level { - return l.GetLevel() -} - -// Global returns the global logger instance. -// Deprecated: Use GetGlobal instead. -// -// logger := Global() -func Global() *Logger { - return GetGlobal() -} diff --git a/logging/compat_test.go b/logging/compat_test.go deleted file mode 100644 index 1310c17..0000000 --- a/logging/compat_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package logging - -import ( - "bytes" - "testing" - - core "dappco.re/go/core" -) - -func TestGetLevel_Good(t *testing.T) { - logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) - if logger.GetLevel() != LevelWarn { - t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) - } - if logger.GetLevel() != logger.Level() { - t.Error("GetLevel() and Level() should return the same value") - } -} - -func TestWithComponent_Good(t *testing.T) { - var buf bytes.Buffer - parent := New(Config{Output: &buf, Level: LevelInfo}) - child := parent.WithComponent("TestChild") - child.Info("test message") - if !core.Contains(buf.String(), "[TestChild]") { - t.Error("WithComponent should set the component name") - } -} - -func TestGetGlobal_Good(t *testing.T) { - var buf bytes.Buffer - logger := New(Config{Output: &buf, Level: LevelInfo}) - SetGlobal(logger) - got := GetGlobal() - if got != Global() { - t.Error("GetGlobal() and Global() should return the same logger") - } - SetGlobal(New(DefaultConfig())) -} diff --git a/node/compat.go b/node/compat.go deleted file mode 100644 index fce12ad..0000000 --- a/node/compat.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides deprecated compatibility aliases and type aliases -// for spec compatibility. Canonical names are defined in their respective -// source files; these aliases exist so that code written against the spec -// documentation continues to compile. - -package node - -// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. -// Deprecated: Use NewNodeManagerFromPaths instead. -// -// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - -// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. -// Deprecated: Use NewPeerRegistryFromPath instead. -// -// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - -// RegisterWithTransport installs the worker message handler on the transport. -// Deprecated: Use RegisterOnTransport instead. -// -// worker.RegisterWithTransport() -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} - -// ConnectedPeers returns the number of connected peers. -// Deprecated: Use ConnectedPeerCount instead. -// -// count := transport.ConnectedPeers() -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} - -// GetLogsPayload is an alias for LogsRequestPayload. -// Provided for spec compatibility. -// -// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} -type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go deleted file mode 100644 index 9b197aa..0000000 --- a/node/compat_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewNodeManagerWithPaths_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - - nm, err := NewNodeManagerWithPaths(keyPath, configPath) - require.NoError(t, err) - assert.NotNil(t, nm) - assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") -} - -func TestNewPeerRegistryWithPath_Good(t *testing.T) { - peersPath := t.TempDir() + "/peers.json" - - pr, err := NewPeerRegistryWithPath(peersPath) - require.NoError(t, err) - assert.NotNil(t, pr) - assert.Equal(t, 0, pr.Count()) - pr.Close() -} - -func TestGetLogsPayload_Good(t *testing.T) { - var payload GetLogsPayload - payload.MinerName = "xmrig-0" - payload.Lines = 100 - payload.Since = 1234567890 - - var request LogsRequestPayload = payload - assert.Equal(t, "xmrig-0", request.MinerName) - assert.Equal(t, 100, request.Lines) - assert.Equal(t, int64(1234567890), request.Since) -} - -func TestTransportConnectedPeers_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - peersPath := t.TempDir() + "/peers.json" - - nm, err := NewNodeManagerFromPaths(keyPath, configPath) - require.NoError(t, err) - - pr, err := NewPeerRegistryFromPath(peersPath) - require.NoError(t, err) - defer pr.Close() - - transport := NewTransport(nm, pr, DefaultTransportConfig()) - assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) - assert.Equal(t, 0, transport.ConnectedPeers()) -} diff --git a/node/levin/compat.go b/node/levin/compat.go deleted file mode 100644 index a88bdfb..0000000 --- a/node/levin/compat.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides short-form aliases for Value constructors, -// matching the naming used in the spec documentation. - -package levin - -// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. -// -// value := Uint64Val(42) -func Uint64Val(v uint64) Value { return Uint64Value(v) } - -// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. -// -// value := Uint32Val(42) -func Uint32Val(v uint32) Value { return Uint32Value(v) } - -// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. -// -// value := Uint16Val(42) -func Uint16Val(v uint16) Value { return Uint16Value(v) } - -// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. -// -// value := Uint8Val(42) -func Uint8Val(v uint8) Value { return Uint8Value(v) } - -// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. -// -// value := Int64Val(42) -func Int64Val(v int64) Value { return Int64Value(v) } - -// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. -// -// value := Int32Val(42) -func Int32Val(v int32) Value { return Int32Value(v) } - -// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. -// -// value := Int16Val(42) -func Int16Val(v int16) Value { return Int16Value(v) } - -// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. -// -// value := Int8Val(42) -func Int8Val(v int8) Value { return Int8Value(v) } - -// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. -// -// value := BoolVal(true) -func BoolVal(v bool) Value { return BoolValue(v) } - -// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. -// -// value := DoubleVal(3.14) -func DoubleVal(v float64) Value { return DoubleValue(v) } - -// StringVal creates a Value of TypeString. Short-form alias for StringValue. -// -// value := StringVal([]byte("hello")) -func StringVal(v []byte) Value { return StringValue(v) } - -// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. -// -// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) -func ObjectVal(s Section) Value { return ObjectValue(s) } - -// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. -// -// value := Uint64ArrayVal([]uint64{1, 2, 3}) -func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } - -// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. -// -// value := Uint32ArrayVal([]uint32{1, 2, 3}) -func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } - -// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. -// -// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) -func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } - -// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. -// -// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) -func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go deleted file mode 100644 index 72de5f4..0000000 --- a/node/levin/compat_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -package levin - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestShortFormAliases_Good(t *testing.T) { - assert.Equal(t, Uint64Value(42), Uint64Val(42)) - assert.Equal(t, Uint32Value(42), Uint32Val(42)) - assert.Equal(t, Uint16Value(42), Uint16Val(42)) - assert.Equal(t, Uint8Value(42), Uint8Val(42)) - assert.Equal(t, Int64Value(-42), Int64Val(-42)) - assert.Equal(t, Int32Value(-42), Int32Val(-42)) - assert.Equal(t, Int16Value(-42), Int16Val(-42)) - assert.Equal(t, Int8Value(-42), Int8Val(-42)) - assert.Equal(t, BoolValue(true), BoolVal(true)) - assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) - assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) - - section := Section{"key": StringValue([]byte("value"))} - assert.Equal(t, ObjectValue(section), ObjectVal(section)) - - assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) - assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) - assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) - assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) -} - -func TestShortFormAliasesRoundTrip_Good(t *testing.T) { - s := Section{ - "u64": Uint64Val(0xCAFEBABE), - "str": StringVal([]byte("hello")), - "flag": BoolVal(true), - "obj": ObjectVal(Section{"nested": Int32Val(42)}), - } - - data, err := EncodeStorage(s) - require.NoError(t, err) - - decoded, err := DecodeStorage(data) - require.NoError(t, err) - - u64, err := decoded["u64"].AsUint64() - require.NoError(t, err) - assert.Equal(t, uint64(0xCAFEBABE), u64) - - str, err := decoded["str"].AsString() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), str) - - flag, err := decoded["flag"].AsBool() - require.NoError(t, err) - assert.True(t, flag) - - obj, err := decoded["obj"].AsSection() - require.NoError(t, err) - nested, err := obj["nested"].AsInt32() - require.NoError(t, err) - assert.Equal(t, int32(42), nested) -} diff --git a/specs/logging.md b/specs/logging.md index 31de890..b3ba1ef 100644 --- a/specs/logging.md +++ b/specs/logging.md @@ -43,7 +43,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | | `DefaultConfig` | `func DefaultConfig() Config` | Returns the default configuration: stderr output, `LevelInfo`, and no component label. | -| `New` | `func New(cfg Config) *Logger` | Creates a `Logger` from `cfg`, substituting the default stderr writer when `cfg.Output` is `nil`. | +| `New` | `func New(config Config) *Logger` | Creates a `Logger` from `config`, substituting the default stderr writer when `config.Output` is `nil`. | | `SetGlobal` | `func SetGlobal(l *Logger)` | Replaces the package-level global logger instance. | | `GetGlobal` | `func GetGlobal() *Logger` | Returns the current package-level global logger. | | `SetGlobalLevel` | `func SetGlobalLevel(level Level)` | Updates the minimum severity on the current global logger. | @@ -67,8 +67,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | -| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. Preferred over `WithComponent`. | -| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Deprecated compatibility alias for `ComponentLogger`. | +| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. | | `SetLevel` | `func (l *Logger) SetLevel(level Level)` | Sets the minimum severity that the logger will emit. | | `GetLevel` | `func (l *Logger) GetLevel() Level` | Returns the current minimum severity. | | `Debug` | `func (l *Logger) Debug(msg string, fields ...Fields)` | Logs `msg` at debug level after merging any supplied field maps. | diff --git a/specs/node-levin.md b/specs/node-levin.md index b5e8912..ca6b93c 100644 --- a/specs/node-levin.md +++ b/specs/node-levin.md @@ -48,7 +48,7 @@ type Value struct { } ``` -Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Val`, `StringVal`, and `ObjectArrayVal` create correctly-typed instances. +Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Value`, `StringValue`, and `ObjectArrayValue` create correctly-typed instances. ## Functions @@ -68,22 +68,22 @@ Tagged portable-storage value. The exported `Type` field identifies which intern | Name | Signature | Description | | --- | --- | --- | -| `Uint64Val` | `func Uint64Val(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | -| `Uint32Val` | `func Uint32Val(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | -| `Uint16Val` | `func Uint16Val(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | -| `Uint8Val` | `func Uint8Val(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | -| `Int64Val` | `func Int64Val(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | -| `Int32Val` | `func Int32Val(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | -| `Int16Val` | `func Int16Val(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | -| `Int8Val` | `func Int8Val(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | -| `BoolVal` | `func BoolVal(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | -| `DoubleVal` | `func DoubleVal(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | -| `StringVal` | `func StringVal(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | -| `ObjectVal` | `func ObjectVal(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | -| `Uint64ArrayVal` | `func Uint64ArrayVal(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | -| `Uint32ArrayVal` | `func Uint32ArrayVal(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | -| `StringArrayVal` | `func StringArrayVal(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | -| `ObjectArrayVal` | `func ObjectArrayVal(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | +| `Uint64Value` | `func Uint64Value(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | +| `Uint32Value` | `func Uint32Value(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | +| `Uint16Value` | `func Uint16Value(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | +| `Uint8Value` | `func Uint8Value(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | +| `Int64Value` | `func Int64Value(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | +| `Int32Value` | `func Int32Value(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | +| `Int16Value` | `func Int16Value(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | +| `Int8Value` | `func Int8Value(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | +| `BoolValue` | `func BoolValue(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | +| `DoubleValue` | `func DoubleValue(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | +| `StringValue` | `func StringValue(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | +| `ObjectValue` | `func ObjectValue(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | +| `Uint64ArrayValue` | `func Uint64ArrayValue(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | +| `Uint32ArrayValue` | `func Uint32ArrayValue(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | +| `StringArrayValue` | `func StringArrayValue(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | +| `ObjectArrayValue` | `func ObjectArrayValue(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | ### `*Connection` methods diff --git a/specs/node.md b/specs/node.md index 87324b3..99f6eb8 100644 --- a/specs/node.md +++ b/specs/node.md @@ -32,7 +32,7 @@ | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WSPath string; TLSCertPath string; TLSKeyPath string; MaxConns int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -43,7 +43,7 @@ | `DeployPayload` | `struct{ BundleType string; Data []byte; Checksum string; Name string }` | Deployment request carrying STIM-encrypted bundle bytes (or other bundle data), checksum, and logical name. | | `DisconnectPayload` | `struct{ Reason string; Code int }` | Disconnect notice with human-readable reason and optional disconnect code. | | `ErrorPayload` | `struct{ Code int; Message string; Details string }` | Payload used by `MsgError` responses. | -| `GetLogsPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | +| `LogsRequestPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | | `HandshakeAckPayload` | `struct{ Identity NodeIdentity; ChallengeResponse []byte; Accepted bool; Reason string }` | Handshake reply containing the responder identity, optional challenge response, acceptance flag, and optional rejection reason. | | `HandshakePayload` | `struct{ Identity NodeIdentity; Challenge []byte; Version string }` | Handshake request containing node identity, optional authentication challenge, and protocol version. | | `LogsPayload` | `struct{ MinerName string; Lines []string; HasMore bool }` | Returned miner log lines plus an indicator that more lines are available. | @@ -88,17 +88,15 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `:9091`, `/ws`, `MaxConns=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | | `NewNodeManager` | `func NewNodeManager() (*NodeManager, error)` | Resolves XDG key and config paths, then loads an existing identity if present. | | `NewNodeManagerFromPaths` | `func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager from explicit key and config paths. | -| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Deprecated compatibility alias for `NewNodeManagerFromPaths`. | | `NewPeerRateLimiter` | `func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter` | Creates a token bucket seeded with `maxTokens` and refilled at `refillRate` tokens per second. | | `NewPeerRegistry` | `func NewPeerRegistry() (*PeerRegistry, error)` | Resolves the XDG peers path, loads any persisted peers, and builds the selection KD-tree. | | `NewPeerRegistryFromPath` | `func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | -| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Deprecated compatibility alias for `NewPeerRegistryFromPath`. | | `NewTransport` | `func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport` | Creates a transport with lifecycle context, a 5-minute message deduplicator, and a WebSocket upgrader that only accepts local origins. | | `NewWorker` | `func NewWorker(node *NodeManager, transport *Transport) *Worker` | Creates a worker, records its start time for uptime reporting, and defaults `DataDir` to `xdg.DataHome`. | @@ -213,7 +211,7 @@ | `Connections` | `func (t *Transport) Connections() iter.Seq[*PeerConnection]` | Returns an iterator over active peer connections. | | `Broadcast` | `func (t *Transport) Broadcast(msg *Message) error` | Sends `msg` to every connected peer except the sender identified by `msg.From`. | | `GetConnection` | `func (t *Transport) GetConnection(peerID string) *PeerConnection` | Returns the active connection for `peerID`, or `nil` when not connected. | -| `ConnectedPeers` | `func (t *Transport) ConnectedPeers() int` | Returns the number of active peer connections. | +| `ConnectedPeerCount` | `func (t *Transport) ConnectedPeerCount() int` | Returns the number of active peer connections. | ### `*PeerConnection` methods @@ -231,7 +229,6 @@ | `SetProfileManager` | `func (w *Worker) SetProfileManager(manager ProfileManager)` | Installs the profile manager used during deployment handling. | | `HandleMessage` | `func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message)` | Dispatches supported message types, sends normal replies on success, and emits `MsgError` responses when a handled command fails. | | `RegisterOnTransport` | `func (w *Worker) RegisterOnTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | -| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Deprecated compatibility alias for `RegisterOnTransport`. | ### `*ProtocolError` methods diff --git a/ueps/compat.go b/ueps/compat.go deleted file mode 100644 index 2b81812..0000000 --- a/ueps/compat.go +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package ueps - -// Short-form tag aliases for spec compatibility. -const ( - // TagCurrentLay is a short-form alias for TagCurrentLayer. - TagCurrentLay = TagCurrentLayer - // TagTargetLay is a short-form alias for TagTargetLayer. - TagTargetLay = TagTargetLayer -) diff --git a/ueps/compat_test.go b/ueps/compat_test.go deleted file mode 100644 index d57fc01..0000000 --- a/ueps/compat_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package ueps - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTagAliases_Good(t *testing.T) { - assert.Equal(t, TagCurrentLayer, TagCurrentLay) - assert.Equal(t, TagTargetLayer, TagTargetLay) - assert.Equal(t, 0x02, TagCurrentLay) - assert.Equal(t, 0x03, TagTargetLay) -} -- 2.45.3 From 0d9f03031c439c8757a895b8ceb91efafd9f6fd1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:29:51 +0000 Subject: [PATCH 50/60] refactor(logging): remove stale alias wording Co-Authored-By: Virgil --- logging/logger.go | 9 +-------- logging/logger_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index 2746881..fed3fa9 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -27,7 +27,7 @@ const ( LevelError ) -// String returns the string representation of the log level. +// label := LevelWarn.String() func (l Level) String() string { switch l { case LevelDebug: @@ -133,7 +133,6 @@ func (stderrWriter) Write(p []byte) (int, error) { var defaultOutput io.Writer = stderrWriter{} -// log writes a log message at the specified level. func (l *Logger) log(level Level, message string, fields Fields) { l.mu.Lock() defer l.mu.Unlock() @@ -142,7 +141,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { return } - // Build the log line sb := core.NewBuilder() timestamp := time.Now().Format("2006/01/02 15:04:05") sb.WriteString(timestamp) @@ -159,7 +157,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { sb.WriteString(" ") sb.WriteString(message) - // Add fields if present if len(fields) > 0 { sb.WriteString(" |") for k, v := range fields { @@ -226,8 +223,6 @@ func mergeFields(fields []Fields) Fields { return result } -// --- Global logger for convenience --- - var ( globalLogger = New(DefaultConfig()) globalMu sync.RWMutex @@ -260,8 +255,6 @@ func SetGlobalLevel(level Level) { globalLogger.SetLevel(level) } -// Global convenience functions that use the global logger - // Debug logs a debug message using the global logger. // // Debug("connected", Fields{"peer_id": "node-1"}) diff --git a/logging/logger_test.go b/logging/logger_test.go index b46aaec..c553130 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -101,15 +101,15 @@ func TestLogger_ComponentLogger_Good(t *testing.T) { child := parent.ComponentLogger("ChildComponent") child.Info("child message") - alias := parent.ComponentLogger("AliasComponent") - alias.Info("alias message") + secondaryLogger := parent.ComponentLogger("SecondaryComponent") + secondaryLogger.Info("secondary message") output := buf.String() if !core.Contains(output, "[ChildComponent]") { t.Error("Derived component name should appear") } - if !core.Contains(output, "[AliasComponent]") { - t.Error("Compatibility alias should preserve the component name") + if !core.Contains(output, "[SecondaryComponent]") { + t.Error("Secondary component should preserve the component name") } } -- 2.45.3 From 9523bd1e9afea188165762c3c160de309d99af7a Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:36:45 +0000 Subject: [PATCH 51/60] refactor(node): align AX comments across public APIs Co-Authored-By: Virgil --- logging/logger.go | 84 ++++++++++------------------------------ node/controller.go | 4 +- node/dispatcher.go | 70 +++++++-------------------------- node/errors.go | 8 +--- node/identity.go | 6 +-- node/levin/connection.go | 36 +++++------------ node/message.go | 14 ++----- node/peer.go | 6 +-- node/protocol.go | 2 +- node/transport.go | 72 +++++++++------------------------- node/worker.go | 33 ++++------------ 11 files changed, 84 insertions(+), 251 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index fed3fa9..bbaac12 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -11,9 +11,7 @@ import ( core "dappco.re/go/core" ) -// Level represents the severity of a log message. -// -// level := LevelInfo +// level := LevelInfo type Level int const ( @@ -43,9 +41,7 @@ func (l Level) String() string { } } -// Logger provides structured logging with configurable output and level. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) type Logger struct { mu sync.RWMutex output io.Writer @@ -53,18 +49,14 @@ type Logger struct { component string } -// Config holds configuration for creating a new Logger. -// -// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} +// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} type Config struct { Output io.Writer Level Level Component string } -// DefaultConfig returns the default logger configuration. -// -// config := DefaultConfig() +// config := DefaultConfig() func DefaultConfig() Config { return Config{ Output: defaultOutput, @@ -73,9 +65,7 @@ func DefaultConfig() Config { } } -// New creates a logger from an explicit configuration. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) func New(config Config) *Logger { if config.Output == nil { config.Output = defaultOutput @@ -87,9 +77,7 @@ func New(config Config) *Logger { } } -// ComponentLogger returns a new Logger scoped to one component. -// -// transportLogger := logger.ComponentLogger("transport") +// transportLogger := logger.ComponentLogger("transport") func (l *Logger) ComponentLogger(component string) *Logger { return &Logger{ output: l.output, @@ -98,27 +86,21 @@ func (l *Logger) ComponentLogger(component string) *Logger { } } -// SetLevel changes the minimum log level. -// -// logger.SetLevel(LevelDebug) +// logger.SetLevel(LevelDebug) func (l *Logger) SetLevel(level Level) { l.mu.Lock() defer l.mu.Unlock() l.level = level } -// GetLevel returns the current log level. -// -// level := logger.GetLevel() +// level := logger.GetLevel() func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level } -// Fields represents key-value pairs for structured logging. -// -// fields := Fields{"peer_id": "node-1", "attempt": 2} +// fields := Fields{"peer_id": "node-1", "attempt": 2} type Fields map[string]any type stderrWriter struct{} @@ -228,92 +210,68 @@ var ( globalMu sync.RWMutex ) -// SetGlobal installs the global logger instance. -// -// SetGlobal(New(DefaultConfig())) +// SetGlobal(New(DefaultConfig())) func SetGlobal(l *Logger) { globalMu.Lock() defer globalMu.Unlock() globalLogger = l } -// GetGlobal returns the global logger instance. -// -// logger := GetGlobal() +// logger := GetGlobal() func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger } -// SetGlobalLevel changes the global logger level. -// -// SetGlobalLevel(LevelDebug) +// SetGlobalLevel(LevelDebug) func SetGlobalLevel(level Level) { globalMu.RLock() defer globalMu.RUnlock() globalLogger.SetLevel(level) } -// Debug logs a debug message using the global logger. -// -// Debug("connected", Fields{"peer_id": "node-1"}) +// Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { GetGlobal().Debug(message, fields...) } -// Info logs an informational message using the global logger. -// -// Info("worker started", Fields{"component": "transport"}) +// Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { GetGlobal().Info(message, fields...) } -// Warn logs a warning message using the global logger. -// -// Warn("peer rate limited", Fields{"peer_id": "node-1"}) +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { GetGlobal().Warn(message, fields...) } -// Error logs an error message using the global logger. -// -// Error("send failed", Fields{"peer_id": "node-1"}) +// Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { GetGlobal().Error(message, fields...) } -// Debugf logs a formatted debug message using the global logger. -// -// Debugf("connected peer %s", "node-1") +// Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { GetGlobal().Debugf(format, args...) } -// Infof logs a formatted informational message using the global logger. -// -// Infof("worker %s ready", "node-1") +// Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { GetGlobal().Infof(format, args...) } -// Warnf logs a formatted warning message using the global logger. -// -// Warnf("peer %s is slow", "node-1") +// Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { GetGlobal().Warnf(format, args...) } -// Errorf logs a formatted error message using the global logger. -// -// Errorf("peer %s failed", "node-1") +// Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { GetGlobal().Errorf(format, args...) } -// ParseLevel parses a string into a log level. -// -// level, err := ParseLevel("warn") +// level, err := ParseLevel("warn") func ParseLevel(s string) (Level, error) { switch core.Upper(s) { case "DEBUG": diff --git a/node/controller.go b/node/controller.go index a683738..3d4ebc9 100644 --- a/node/controller.go +++ b/node/controller.go @@ -10,9 +10,7 @@ import ( "dappco.re/go/core/p2p/logging" ) -// Controller drives remote peer operations from a controller node. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { nodeManager *NodeManager peerRegistry *PeerRegistry diff --git a/node/dispatcher.go b/node/dispatcher.go index 126e3b2..1307713 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -10,11 +10,7 @@ import ( "dappco.re/go/core/p2p/ueps" ) -// ThreatScoreThreshold is the maximum allowable threat score. Packets exceeding -// this value are silently dropped by the circuit breaker and logged as threat -// events. The threshold sits at ~76% of the uint16 range (50,000 / 65,535), -// providing headroom for legitimate elevated-risk traffic whilst rejecting -// clearly hostile payloads. +// threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 // Well-known intent identifiers. These correspond to the semantic tokens @@ -32,34 +28,16 @@ const ( // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error -// Dispatcher routes verified UEPS packets to registered intent handlers. -// It enforces a threat circuit breaker before routing: any packet whose -// ThreatScore exceeds ThreatScoreThreshold is dropped and logged. -// -// dispatcher := NewDispatcher() -// -// Design decisions: -// -// - Handlers are registered per IntentID (1:1 mapping). -// -// - Unknown intents are logged at WARN level and silently dropped (no error -// returned to the caller) to avoid back-pressure on the transport layer. -// -// - High-threat packets are dropped silently (logged at WARN) rather than -// returning an error, consistent with the "don't even parse the payload" -// philosophy from the original stub. -// -// - The dispatcher is safe for concurrent use; a RWMutex protects the -// handler map. +// dispatcher := NewDispatcher() +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) +// err := dispatcher.Dispatch(packet) type Dispatcher struct { handlers map[byte]IntentHandler mu sync.RWMutex log *logging.Logger } -// NewDispatcher creates a Dispatcher with no registered handlers. -// -// dispatcher := NewDispatcher() +// dispatcher := NewDispatcher() func NewDispatcher() *Dispatcher { return &Dispatcher{ handlers: make(map[byte]IntentHandler), @@ -70,12 +48,7 @@ func NewDispatcher() *Dispatcher { } } -// RegisterHandler associates an IntentHandler with a specific IntentID. -// Replacing an existing handler is allowed — the new handler takes effect immediately. -// -// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { -// return processComputeJob(packet.Payload) -// }) +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { d.mu.Lock() defer d.mu.Unlock() @@ -85,11 +58,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// Handlers returns an iterator over all registered intent handlers. -// -// for intentID, handler := range dispatcher.Handlers() { -// log.Printf("registered intent 0x%02X", intentID) -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() @@ -103,17 +75,7 @@ func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { } } -// Dispatch routes a parsed UEPS packet through the threat circuit breaker -// and then to the appropriate intent handler. -// -// Behaviour: -// - Returns ErrorThreatScoreExceeded if the packet's ThreatScore exceeds the -// threshold (packet is dropped and logged). -// - Returns ErrorUnknownIntent if no handler is registered for the IntentID -// (packet is dropped and logged). -// - Returns nil on successful delivery to a handler, or any error the -// handler itself returns. -// - A nil packet returns ErrorNilPacket immediately. +// err := dispatcher.Dispatch(packet) func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { if packet == nil { return ErrorNilPacket @@ -146,17 +108,13 @@ func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { return handler(packet) } -// Sentinel errors returned by Dispatch. var ( - // ErrorThreatScoreExceeded is returned when a packet's ThreatScore exceeds - // the safety threshold. + // err := ErrorThreatScoreExceeded ErrorThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) - // ErrorUnknownIntent is returned when no handler is registered for the - // packet's IntentID. + // err := ErrorUnknownIntent ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - // ErrorNilPacket is returned when a nil packet is passed to Dispatch. + // err := ErrorNilPacket ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) - diff --git a/node/errors.go b/node/errors.go index f660be1..302a34a 100644 --- a/node/errors.go +++ b/node/errors.go @@ -2,14 +2,10 @@ package node import core "dappco.re/go/core" -// Shared error sentinels for the node package. var ( - // ErrorIdentityNotInitialized is returned when a node operation requires - // a node identity but none has been generated or loaded. + // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // ErrorMinerManagerNotConfigured is returned when a miner operation is - // attempted but no MinerManager has been set on the Worker. + // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) - diff --git a/node/identity.go b/node/identity.go index fc2aa2d..1309281 100644 --- a/node/identity.go +++ b/node/identity.go @@ -73,9 +73,7 @@ type NodeIdentity struct { Role NodeRole `json:"role"` } -// NodeManager handles node identity operations including key generation and storage. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() type NodeManager struct { identity *NodeIdentity privateKey []byte // Never serialized to JSON @@ -126,7 +124,7 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } -// HasIdentity returns true if a node identity has been initialized. +// hasIdentity := nodeManager.HasIdentity() func (n *NodeManager) HasIdentity() bool { n.mu.RLock() defer n.mu.RUnlock() diff --git a/node/levin/connection.go b/node/levin/connection.go index 46670f2..74a3a04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -10,26 +10,22 @@ import ( "time" ) -// Levin protocol flags. +// flags := FlagRequest | FlagResponse const ( FlagRequest uint32 = 0x00000001 FlagResponse uint32 = 0x00000002 ) -// LevinProtocolVersion is the protocol version field written into every header. +// header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// Default timeout values for Connection read and write operations. +// conn.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// Connection wraps a net.Conn and provides framed Levin packet I/O. -// All writes are serialised by an internal mutex, making it safe to call -// WritePacket and WriteResponse concurrently from multiple goroutines. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -45,9 +41,7 @@ type Connection struct { writeMutex sync.Mutex } -// NewConnection creates a Connection that wraps conn with sensible defaults. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) func NewConnection(conn net.Conn) *Connection { return &Connection{ MaxPayloadSize: MaxPayloadSize, @@ -57,9 +51,7 @@ func NewConnection(conn net.Conn) *Connection { } } -// WritePacket sends a Levin request or notification. -// -// err := conn.WritePacket(CommandPing, payload, true) +// err := conn.WritePacket(CommandPing, payload, true) func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, @@ -73,9 +65,7 @@ func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool return c.writeFrame(&header, payload) } -// WriteResponse sends a Levin response packet with the given return code. -// -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) +// err := conn.WriteResponse(CommandPing, payload, ReturnOK) func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, @@ -113,9 +103,7 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// ReadPacket reads and validates the next Levin packet. -// -// header, payload, err := conn.ReadPacket() +// header, payload, err := conn.ReadPacket() func (c *Connection) ReadPacket() (Header, []byte, error) { if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return Header{}, nil, err @@ -150,16 +138,12 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { return header, payload, nil } -// Close closes the underlying network connection. -// -// err := conn.Close() +// err := conn.Close() func (c *Connection) Close() error { return c.conn.Close() } -// RemoteAddr returns the remote address of the underlying connection as a string. -// -// addr := conn.RemoteAddr() +// addr := conn.RemoteAddr() func (c *Connection) RemoteAddr() string { return c.conn.RemoteAddr().String() } diff --git a/node/message.go b/node/message.go index 4931d8f..1640fde 100644 --- a/node/message.go +++ b/node/message.go @@ -126,9 +126,7 @@ func NewMessage(messageType MessageType, from, to string, payload any) (*Message }, nil } -// Reply creates a response message that points back to the original. -// -// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) +// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) { reply, err := NewMessage(messageType, m.To, m.From, payload) if err != nil { @@ -138,10 +136,8 @@ func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) return reply, nil } -// ParsePayload decodes the payload into the supplied target. -// -// var ping PingPayload -// err := message.ParsePayload(&ping) +// var ping PingPayload +// err := message.ParsePayload(&ping) func (m *Message) ParsePayload(target any) error { if m.Payload == nil { return nil @@ -295,9 +291,7 @@ const ( ErrorCodeTimeout = 1005 ) -// NewErrorMessage builds an error response message for an existing request. -// -// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") +// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { errorMessage, err := NewMessage(MessageError, from, to, ErrorPayload{ Code: code, diff --git a/node/peer.go b/node/peer.go index aeafe82..b44348a 100644 --- a/node/peer.go +++ b/node/peer.go @@ -53,7 +53,7 @@ const peerRegistrySaveDebounceInterval = 5 * time.Second type PeerAuthMode int const ( - // PeerAuthOpen allows any peer to connect (original behavior) + // PeerAuthOpen allows any peer to connect. PeerAuthOpen PeerAuthMode = iota // PeerAuthAllowlist only allows pre-registered peers or those with allowed public keys PeerAuthAllowlist @@ -98,9 +98,7 @@ func validatePeerName(name string) error { return nil } -// PeerRegistry manages known peers with KD-tree based selection. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() type PeerRegistry struct { peers map[string]*Peer kdTree *poindexter.KDTree[string] // KD-tree with peer ID as payload diff --git a/node/protocol.go b/node/protocol.go index 34f255d..72c1624 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -63,7 +63,7 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, return nil } -// DefaultResponseHandler is the default response handler instance. +// handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. diff --git a/node/transport.go b/node/transport.go index 4acdfff..9d9c989 100644 --- a/node/transport.go +++ b/node/transport.go @@ -144,9 +144,7 @@ func (d *MessageDeduplicator) Cleanup() { } } -// Transport manages WebSocket connections with SMSG encryption. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) type Transport struct { config TransportConfig httpServer *http.Server @@ -163,9 +161,7 @@ type Transport struct { waitGroup sync.WaitGroup } -// PeerRateLimiter implements a simple token bucket rate limiter per peer. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) type PeerRateLimiter struct { availableTokens int capacity int @@ -187,7 +183,7 @@ func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { } } -// Allow checks if a message is allowed and consumes a token if so +// allowed := rateLimiter.Allow() func (r *PeerRateLimiter) Allow() bool { r.mutex.Lock() defer r.mutex.Unlock() @@ -209,9 +205,7 @@ func (r *PeerRateLimiter) Allow() bool { return false } -// PeerConnection represents an active connection to a peer. -// -// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} +// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { Peer *Peer Conn *websocket.Conn @@ -224,9 +218,7 @@ type PeerConnection struct { rateLimiter *PeerRateLimiter // Per-peer message rate limiting } -// NewTransport creates a WebSocket transport for a node and peer registry. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) @@ -310,9 +302,7 @@ func (t *Transport) agentUserAgent() string { ) } -// Start opens the WebSocket listener and background maintenance loops. -// -// err := transport.Start() +// err := transport.Start() func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.webSocketPath(), t.handleWebSocketUpgrade) @@ -381,9 +371,7 @@ func (t *Transport) Start() error { return nil } -// Stop closes active connections and shuts the transport down cleanly. -// -// err := transport.Stop() +// err := transport.Stop() func (t *Transport) Stop() error { t.cancelLifecycle() @@ -410,18 +398,14 @@ func (t *Transport) Stop() error { return nil } -// OnMessage installs the handler for incoming messages before Start. -// -// transport.OnMessage(worker.HandleMessage) +// transport.OnMessage(worker.HandleMessage) func (t *Transport) OnMessage(handler MessageHandler) { t.mutex.Lock() defer t.mutex.Unlock() t.messageHandler = handler } -// Connect dials a peer, completes the handshake, and starts the session loops. -// -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -485,9 +469,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return pc, nil } -// Send transmits an encrypted message to a connected peer. -// -// err := transport.Send("worker-1", message) +// err := transport.Send("worker-1", message) func (t *Transport) Send(peerID string, msg *Message) error { t.mutex.RLock() pc, exists := t.connections[peerID] @@ -500,11 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// Connections returns an iterator over all active peer connections. -// -// for pc := range transport.Connections() { -// log.Printf("connected: %s", pc.Peer.ID) -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -518,10 +498,7 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } } -// Broadcast sends a message to every connected peer except the sender. -// The sender (msg.From) is excluded to prevent echo. -// -// err := transport.Broadcast(announcement) +// err := transport.Broadcast(announcement) func (t *Transport) Broadcast(msg *Message) error { conns := slices.Collect(t.Connections()) @@ -537,9 +514,7 @@ func (t *Transport) Broadcast(msg *Message) error { return lastErr } -// GetConnection returns an active connection to a peer. -// -// connection := transport.GetConnection("worker-1") +// connection := transport.GetConnection("worker-1") func (t *Transport) GetConnection(peerID string) *PeerConnection { t.mutex.RLock() defer t.mutex.RUnlock() @@ -973,9 +948,7 @@ func (t *Transport) removeConnection(pc *PeerConnection) { pc.Close() } -// Send sends an encrypted message over the connection. -// -// err := peerConnection.Send(message) +// err := peerConnection.Send(message) func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() @@ -993,9 +966,7 @@ func (pc *PeerConnection) Send(msg *Message) error { return pc.Conn.WriteMessage(websocket.BinaryMessage, data) } -// Close closes the connection. -// -// err := peerConnection.Close() +// err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { @@ -1021,9 +992,7 @@ const ( DisconnectShutdown = 1004 // Server shutdown ) -// GracefulClose sends a disconnect message before closing the connection. -// -// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) +// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { @@ -1090,12 +1059,9 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeerCount returns the number of connected peers. -// -// count := transport.ConnectedPeerCount() +// count := transport.ConnectedPeerCount() func (t *Transport) ConnectedPeerCount() int { t.mutex.RLock() defer t.mutex.RUnlock() return len(t.connections) } - diff --git a/node/worker.go b/node/worker.go index 711c16b..3de7277 100644 --- a/node/worker.go +++ b/node/worker.go @@ -10,10 +10,7 @@ import ( "github.com/adrg/xdg" ) -// MinerManager interface for the mining package integration. -// This allows the node package to interact with mining.Manager without import cycles. -// -// var minerManager MinerManager +// var minerManager MinerManager type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error @@ -21,9 +18,7 @@ type MinerManager interface { GetMiner(name string) (MinerInstance, error) } -// MinerInstance represents a running miner for stats collection. -// -// var miner MinerInstance +// var miner MinerInstance type MinerInstance interface { GetName() string GetType() string @@ -31,17 +26,13 @@ type MinerInstance interface { GetConsoleHistory(lines int) []string } -// ProfileManager interface for profile operations. -// -// var profileManager ProfileManager +// var profileManager ProfileManager type ProfileManager interface { GetProfile(id string) (any, error) SaveProfile(profile any) error } -// Worker handles incoming messages on a worker node. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) type Worker struct { nodeManager *NodeManager transport *Transport @@ -51,9 +42,7 @@ type Worker struct { DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) } -// NewWorker creates a new Worker instance. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { return &Worker{ nodeManager: nodeManager, @@ -63,23 +52,17 @@ func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { } } -// SetMinerManager attaches the miner manager used for miner operations. -// -// worker.SetMinerManager(minerManager) +// worker.SetMinerManager(minerManager) func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager } -// SetProfileManager attaches the profile manager used for profile operations. -// -// worker.SetProfileManager(profileManager) +// worker.SetProfileManager(profileManager) func (w *Worker) SetProfileManager(manager ProfileManager) { w.profileManager = manager } -// HandleMessage routes an incoming message to the correct worker handler. -// -// worker.HandleMessage(peerConnection, message) +// worker.HandleMessage(peerConnection, message) func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) { var response *Message var err error -- 2.45.3 From 314c8ed4704d6ba35fdd83fe783b1b6e674f63e5 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:42:04 +0000 Subject: [PATCH 52/60] fix(transport): serialise graceful websocket close Co-Authored-By: Virgil --- node/transport.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/node/transport.go b/node/transport.go index 9d9c989..155d900 100644 --- a/node/transport.go +++ b/node/transport.go @@ -482,9 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// for pc := range transport.Connections() { -// _ = pc -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -953,6 +953,10 @@ func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + return pc.sendLocked(msg) +} + +func (pc *PeerConnection) sendLocked(msg *Message) error { data, err := pc.transport.encryptMessage(msg, pc.SharedSecret) if err != nil { return err @@ -996,10 +1000,10 @@ const ( func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { + pc.writeMutex.Lock() + defer pc.writeMutex.Unlock() + // Try to send disconnect message (best effort). - // Note: we must NOT call SetWriteDeadline outside writeMutex — Send() - // already manages write deadlines under the lock. Setting it here - // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() if identity != nil { @@ -1009,7 +1013,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) if msgErr == nil { - pc.Send(msg) + _ = pc.sendLocked(msg) } } } -- 2.45.3 From de7e866acf224bd36825464f5f61c8d65f807312 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:48:17 +0000 Subject: [PATCH 53/60] refactor(p2p): align AX comments and harden UEPS parsing Co-Authored-By: Virgil --- logging/logger.go | 24 +++++++--------- ueps/packet.go | 54 +++++++++++------------------------- ueps/packet_coverage_test.go | 29 +++++++++++++++++++ ueps/reader.go | 37 ++++++++++++------------ 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index bbaac12..0f463b7 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -1,4 +1,4 @@ -// Package logging provides structured logging with log levels and fields. +// logger := New(DefaultConfig()) package logging import ( @@ -15,13 +15,9 @@ import ( type Level int const ( - // LevelDebug is the most verbose log level. LevelDebug Level = iota - // LevelInfo is for general informational messages. LevelInfo - // LevelWarn is for warning messages. LevelWarn - // LevelError is for error messages. LevelError ) @@ -153,47 +149,47 @@ func (l *Logger) log(level Level, message string, fields Fields) { _, _ = l.output.Write([]byte(sb.String())) } -// Debug logs a debug message. +// Debug("connected", Fields{"peer_id": "node-1"}) func (l *Logger) Debug(message string, fields ...Fields) { l.log(LevelDebug, message, mergeFields(fields)) } -// Info logs an informational message. +// Info("worker started", Fields{"component": "transport"}) func (l *Logger) Info(message string, fields ...Fields) { l.log(LevelInfo, message, mergeFields(fields)) } -// Warn logs a warning message. +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func (l *Logger) Warn(message string, fields ...Fields) { l.log(LevelWarn, message, mergeFields(fields)) } -// Error logs an error message. +// Error("send failed", Fields{"peer_id": "node-1"}) func (l *Logger) Error(message string, fields ...Fields) { l.log(LevelError, message, mergeFields(fields)) } -// Debugf logs a formatted debug message. +// Debugf("connected peer %s", "node-1") func (l *Logger) Debugf(format string, args ...any) { l.log(LevelDebug, core.Sprintf(format, args...), nil) } -// Infof logs a formatted informational message. +// Infof("worker %s ready", "node-1") func (l *Logger) Infof(format string, args ...any) { l.log(LevelInfo, core.Sprintf(format, args...), nil) } -// Warnf logs a formatted warning message. +// Warnf("peer %s is slow", "node-1") func (l *Logger) Warnf(format string, args ...any) { l.log(LevelWarn, core.Sprintf(format, args...), nil) } -// Errorf logs a formatted error message. +// Errorf("peer %s failed", "node-1") func (l *Logger) Errorf(format string, args ...any) { l.log(LevelError, core.Sprintf(format, args...), nil) } -// mergeFields combines multiple Fields maps into one. +// fields := mergeFields([]Fields{{"peer_id": "node-1"}, {"attempt": 2}}) func mergeFields(fields []Fields) Fields { if len(fields) == 0 { return nil diff --git a/ueps/packet.go b/ueps/packet.go index 45ed113..f65a7b4 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -10,60 +10,49 @@ import ( core "dappco.re/go/core" ) -// TLV Types const ( TagVersion = 0x01 TagCurrentLayer = 0x02 TagTargetLayer = 0x03 TagIntent = 0x04 TagThreatScore = 0x05 - TagHMAC = 0x06 // The Signature - TagPayload = 0xFF // The Data + TagHMAC = 0x06 + TagPayload = 0xFF ) -// UEPSHeader represents the conscious routing metadata. -// -// header := UEPSHeader{IntentID: 0x01} +// header := UEPSHeader{Version: 0x09, CurrentLayer: 5, TargetLayer: 5, IntentID: 0x01} type UEPSHeader struct { - Version uint8 // Default 0x09 + Version uint8 CurrentLayer uint8 TargetLayer uint8 - IntentID uint8 // Semantic Token - ThreatScore uint16 // 0-65535 + IntentID uint8 + ThreatScore uint16 } -// PacketBuilder builds a signed UEPS frame from a concrete intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) type PacketBuilder struct { Header UEPSHeader Payload []byte } -// NewBuilder creates a packet builder for a specific intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { return &PacketBuilder{ Header: UEPSHeader{ - Version: 0x09, // IPv9 - CurrentLayer: 5, // Application - TargetLayer: 5, // Application + Version: 0x09, + CurrentLayer: 5, + TargetLayer: 5, IntentID: intentID, - ThreatScore: 0, // Assumed innocent until proven guilty + ThreatScore: 0, }, Payload: payload, } } -// MarshalAndSign signs a packet with a shared secret. -// -// frame, err := builder.MarshalAndSign(sharedSecret) +// frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { buf := new(bytes.Buffer) - // 1. Write Standard Header Tags (0x01 - 0x05) - // We write these first because they are part of what we sign. if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } @@ -77,30 +66,21 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return nil, err } - // Threat Score is uint16, needs binary packing tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { return nil, err } - // 2. Calculate HMAC - // The signature covers: Existing Header TLVs + The Payload - // It does NOT cover the HMAC TLV tag itself (obviously) mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) // The headers so far - mac.Write(p.Payload) // The data + mac.Write(buf.Bytes()) + mac.Write(p.Payload) signature := mac.Sum(nil) - // 3. Write HMAC TLV (0x06) - // Length is 32 bytes for SHA256 if err := writeTLV(buf, TagHMAC, signature); err != nil { return nil, err } - // 4. Write Payload TLV (0xFF) - // Fixed: Now uses writeTLV which provides a 2-byte length prefix. - // This prevents the io.ReadAll DoS and allows multiple packets in a stream. if err := writeTLV(buf, TagPayload, p.Payload); err != nil { return nil, err } @@ -108,10 +88,8 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return buf.Bytes(), nil } -// writeTLV writes a single tag-length-value record with a 2-byte length prefix. -// It supports payloads up to 64KB. +// writeTLV(&buf, TagPayload, []byte("hello")) func writeTLV(w io.Writer, tag uint8, value []byte) error { - // Check length constraint (2 byte length = max 65535 bytes) if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } diff --git a/ueps/packet_coverage_test.go b/ueps/packet_coverage_test.go index 6e3eced..ffd2572 100644 --- a/ueps/packet_coverage_test.go +++ b/ueps/packet_coverage_test.go @@ -211,3 +211,32 @@ func TestPacketCoverage_ReadAndVerify_ManualPacket_PayloadReadError_Bad(t *testi require.Error(t, err) assert.Equal(t, io.ErrUnexpectedEOF, err) } + +// TestReadAndVerify_MalformedHeaderTLV_Bad verifies malformed header values +// return an error instead of panicking during TLV reconstruction. +func TestPacketCoverage_ReadAndVerify_MalformedHeaderTLV_Bad(t *testing.T) { + tests := []struct { + name string + frame []byte + wantErr string + }{ + { + name: "ZeroLengthVersion", + frame: []byte{TagVersion, 0x00, 0x00}, + wantErr: "malformed version TLV", + }, + { + name: "ShortThreatScore", + frame: []byte{TagThreatScore, 0x00, 0x01, 0xFF}, + wantErr: "malformed threat score TLV", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(tc.frame)), testSecret) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/ueps/reader.go b/ueps/reader.go index e620ed1..a024c9c 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -11,84 +11,83 @@ import ( core "dappco.re/go/core" ) -// ParsedPacket holds the verified data -// -// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} +// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} type ParsedPacket struct { Header UEPSHeader Payload []byte } -// ReadAndVerify reads a UEPS frame from the stream and validates the HMAC. -// It consumes the stream up to the end of the packet. -// -// packet, err := ReadAndVerify(reader, sharedSecret) +// packet, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(frame)), sharedSecret) func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) { - // Buffer to reconstruct the data for HMAC verification var signedData bytes.Buffer header := UEPSHeader{} var signature []byte var payload []byte - // Loop through TLVs for { - // 1. Read Tag tag, err := r.ReadByte() if err != nil { return nil, err } - // 2. Read Length (2-byte big-endian uint16) lenBuf := make([]byte, 2) if _, err := io.ReadFull(r, lenBuf); err != nil { return nil, err } length := int(binary.BigEndian.Uint16(lenBuf)) - // 3. Read Value value := make([]byte, length) if _, err := io.ReadFull(r, value); err != nil { return nil, err } - // 4. Handle Tag switch tag { case TagVersion: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed version TLV", nil) + } header.Version = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagCurrentLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed current layer TLV", nil) + } header.CurrentLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagTargetLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed target layer TLV", nil) + } header.TargetLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagIntent: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed intent TLV", nil) + } header.IntentID = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagThreatScore: + if len(value) != 2 { + return nil, core.E("ueps.ReadAndVerify", "malformed threat score TLV", nil) + } header.ThreatScore = binary.BigEndian.Uint16(value) signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagHMAC: signature = value - // HMAC tag itself is not part of the signed data case TagPayload: payload = value - // Exit loop after payload (last tag in UEPS frame) - // Note: The HMAC covers the Payload but NOT the TagPayload/Length bytes - // to match the PacketBuilder.MarshalAndSign logic. goto verify default: - // Unknown tag (future proofing), verify it but ignore semantics signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) @@ -100,8 +99,6 @@ verify: return nil, core.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil) } - // 5. Verify HMAC - // Reconstruct: Headers (signedData) + Payload mac := hmac.New(sha256.New, sharedSecret) mac.Write(signedData.Bytes()) mac.Write(payload) -- 2.45.3 From 96d83a4b21a6da984204a431548b4f6f070cbbe3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:56:56 +0000 Subject: [PATCH 54/60] refactor(node): align AX naming across transport and protocol helpers Co-Authored-By: Virgil --- node/buffer_pool.go | 18 +++++----- node/levin/connection.go | 74 ++++++++++++++++++++-------------------- node/levin/header.go | 52 ++++++++++++++-------------- node/levin/storage.go | 56 +++++++++++++++--------------- node/levin/varint.go | 60 ++++++++++++++++---------------- node/transport.go | 60 ++++++++++++++++---------------- ueps/packet.go | 34 +++++++++--------- 7 files changed, 177 insertions(+), 177 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index ace5915..91f160d 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -17,16 +17,16 @@ var bufferPool = sync.Pool{ // getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { - buf := bufferPool.Get().(*bytes.Buffer) - buf.Reset() - return buf + buffer := bufferPool.Get().(*bytes.Buffer) + buffer.Reset() + return buffer } // putBuffer returns a buffer to the pool. -func putBuffer(buf *bytes.Buffer) { +func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) - if buf.Cap() <= 65536 { - bufferPool.Put(buf) + if buffer.Cap() <= 65536 { + bufferPool.Put(buffer) } } @@ -34,9 +34,9 @@ func putBuffer(buf *bytes.Buffer) { // restores the historical no-EscapeHTML behaviour expected by the node package. // Returns a copy of the encoded bytes (safe to use after the function returns). // -// data, err := MarshalJSON(v) -func MarshalJSON(v any) ([]byte, error) { - encoded := core.JSONMarshal(v) +// data, err := MarshalJSON(value) +func MarshalJSON(value any) ([]byte, error) { + encoded := core.JSONMarshal(value) if !encoded.OK { return nil, encoded.Value.(error) } diff --git a/node/levin/connection.go b/node/levin/connection.go index 74a3a04..76bd643 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -19,13 +19,13 @@ const ( // header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// conn.ReadTimeout = DefaultReadTimeout +// connection.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// conn := NewConnection(netConn) +// connection := NewConnection(networkConnection) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -37,65 +37,65 @@ type Connection struct { // WriteTimeout is the deadline applied before each write call. WriteTimeout time.Duration - conn net.Conn - writeMutex sync.Mutex + networkConnection net.Conn + writeMutex sync.Mutex } -// conn := NewConnection(netConn) -func NewConnection(conn net.Conn) *Connection { +// connection := NewConnection(networkConnection) +func NewConnection(connection net.Conn) *Connection { return &Connection{ - MaxPayloadSize: MaxPayloadSize, - ReadTimeout: DefaultReadTimeout, - WriteTimeout: DefaultWriteTimeout, - conn: conn, + MaxPayloadSize: MaxPayloadSize, + ReadTimeout: DefaultReadTimeout, + WriteTimeout: DefaultWriteTimeout, + networkConnection: connection, } } -// err := conn.WritePacket(CommandPing, payload, true) -func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { +// err := connection.WritePacket(CommandPing, payload, true) +func (connection *Connection) WritePacket(commandID uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: expectResponse, - Command: cmd, + Command: commandID, ReturnCode: ReturnOK, Flags: FlagRequest, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) -func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { +// err := connection.WriteResponse(CommandPing, payload, ReturnOK) +func (connection *Connection) WriteResponse(commandID uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: false, - Command: cmd, + Command: commandID, ReturnCode: returnCode, Flags: FlagResponse, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } // writeFrame serialises header + payload and writes them atomically. -func (c *Connection) writeFrame(header *Header, payload []byte) error { - buf := EncodeHeader(header) +func (connection *Connection) writeFrame(header *Header, payload []byte) error { + headerBytes := EncodeHeader(header) - c.writeMutex.Lock() - defer c.writeMutex.Unlock() + connection.writeMutex.Lock() + defer connection.writeMutex.Unlock() - if err := c.conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { + if err := connection.networkConnection.SetWriteDeadline(time.Now().Add(connection.WriteTimeout)); err != nil { return err } - if _, err := c.conn.Write(buf[:]); err != nil { + if _, err := connection.networkConnection.Write(headerBytes[:]); err != nil { return err } if len(payload) > 0 { - if _, err := c.conn.Write(payload); err != nil { + if _, err := connection.networkConnection.Write(payload); err != nil { return err } } @@ -103,15 +103,15 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// header, payload, err := conn.ReadPacket() -func (c *Connection) ReadPacket() (Header, []byte, error) { - if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { +// header, payload, err := connection.ReadPacket() +func (connection *Connection) ReadPacket() (Header, []byte, error) { + if err := connection.networkConnection.SetReadDeadline(time.Now().Add(connection.ReadTimeout)); err != nil { return Header{}, nil, err } // Read header. var headerBytes [HeaderSize]byte - if _, err := io.ReadFull(c.conn, headerBytes[:]); err != nil { + if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err } @@ -121,7 +121,7 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } // Check against the connection-specific payload limit. - if header.PayloadSize > c.MaxPayloadSize { + if header.PayloadSize > connection.MaxPayloadSize { return Header{}, nil, ErrorPayloadTooBig } @@ -131,19 +131,19 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } payload := make([]byte, header.PayloadSize) - if _, err := io.ReadFull(c.conn, payload); err != nil { + if _, err := io.ReadFull(connection.networkConnection, payload); err != nil { return Header{}, nil, err } return header, payload, nil } -// err := conn.Close() -func (c *Connection) Close() error { - return c.conn.Close() +// err := connection.Close() +func (connection *Connection) Close() error { + return connection.networkConnection.Close() } -// addr := conn.RemoteAddr() -func (c *Connection) RemoteAddr() string { - return c.conn.RemoteAddr().String() +// addr := connection.RemoteAddr() +func (connection *Connection) RemoteAddr() string { + return connection.networkConnection.RemoteAddr().String() } diff --git a/node/levin/header.go b/node/levin/header.go index 2f69577..ee4d9ab 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -60,43 +60,43 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises h into a fixed-size 33-byte array (little-endian). +// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). // // encoded := EncodeHeader(header) -func EncodeHeader(h *Header) [HeaderSize]byte { - var buf [HeaderSize]byte - binary.LittleEndian.PutUint64(buf[0:8], h.Signature) - binary.LittleEndian.PutUint64(buf[8:16], h.PayloadSize) - if h.ExpectResponse { - buf[16] = 0x01 +func EncodeHeader(header *Header) [HeaderSize]byte { + var headerBytes [HeaderSize]byte + binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) + binary.LittleEndian.PutUint64(headerBytes[8:16], header.PayloadSize) + if header.ExpectResponse { + headerBytes[16] = 0x01 } else { - buf[16] = 0x00 + headerBytes[16] = 0x00 } - binary.LittleEndian.PutUint32(buf[17:21], h.Command) - binary.LittleEndian.PutUint32(buf[21:25], uint32(h.ReturnCode)) - binary.LittleEndian.PutUint32(buf[25:29], h.Flags) - binary.LittleEndian.PutUint32(buf[29:33], h.ProtocolVersion) - return buf + binary.LittleEndian.PutUint32(headerBytes[17:21], header.Command) + binary.LittleEndian.PutUint32(headerBytes[21:25], uint32(header.ReturnCode)) + binary.LittleEndian.PutUint32(headerBytes[25:29], header.Flags) + binary.LittleEndian.PutUint32(headerBytes[29:33], header.ProtocolVersion) + return headerBytes } // DecodeHeader deserialises a 33-byte array into a Header, validating // the magic signature. // -// header, err := DecodeHeader(buf) -func DecodeHeader(buf [HeaderSize]byte) (Header, error) { - var h Header - h.Signature = binary.LittleEndian.Uint64(buf[0:8]) - if h.Signature != Signature { +// header, err := DecodeHeader(headerBytes) +func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { + var header Header + header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) + if header.Signature != Signature { return Header{}, ErrorBadSignature } - h.PayloadSize = binary.LittleEndian.Uint64(buf[8:16]) - if h.PayloadSize > MaxPayloadSize { + header.PayloadSize = binary.LittleEndian.Uint64(headerBytes[8:16]) + if header.PayloadSize > MaxPayloadSize { return Header{}, ErrorPayloadTooBig } - h.ExpectResponse = buf[16] == 0x01 - h.Command = binary.LittleEndian.Uint32(buf[17:21]) - h.ReturnCode = int32(binary.LittleEndian.Uint32(buf[21:25])) - h.Flags = binary.LittleEndian.Uint32(buf[25:29]) - h.ProtocolVersion = binary.LittleEndian.Uint32(buf[29:33]) - return h, nil + header.ExpectResponse = headerBytes[16] == 0x01 + header.Command = binary.LittleEndian.Uint32(headerBytes[17:21]) + header.ReturnCode = int32(binary.LittleEndian.Uint32(headerBytes[21:25])) + header.Flags = binary.LittleEndian.Uint32(headerBytes[25:29]) + header.ProtocolVersion = binary.LittleEndian.Uint32(headerBytes[29:33]) + return header, nil } diff --git a/node/levin/storage.go b/node/levin/storage.go index a6c74e2..9a6d4ed 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -83,62 +83,62 @@ type Value struct { // Uint64Value creates a Value of TypeUint64. // // value := Uint64Value(42) -func Uint64Value(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } +func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } // Uint32Value creates a Value of TypeUint32. // // value := Uint32Value(42) -func Uint32Value(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } +func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } // Uint16Value creates a Value of TypeUint16. // // value := Uint16Value(42) -func Uint16Value(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } +func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } // Uint8Value creates a Value of TypeUint8. // // value := Uint8Value(42) -func Uint8Value(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } +func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } // Int64Value creates a Value of TypeInt64. // // value := Int64Value(42) -func Int64Value(v int64) Value { return Value{Type: TypeInt64, intVal: v} } +func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } // Int32Value creates a Value of TypeInt32. // // value := Int32Value(42) -func Int32Value(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } +func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } // Int16Value creates a Value of TypeInt16. // // value := Int16Value(42) -func Int16Value(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } +func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } // Int8Value creates a Value of TypeInt8. // // value := Int8Value(42) -func Int8Value(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } +func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } // BoolValue creates a Value of TypeBool. // // value := BoolValue(true) -func BoolValue(v bool) Value { return Value{Type: TypeBool, boolVal: v} } +func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } // DoubleValue creates a Value of TypeDouble. // // value := DoubleValue(3.14) -func DoubleValue(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } +func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } // StringValue creates a Value of TypeString. The slice is not copied. // // value := StringValue([]byte("hello")) -func StringValue(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } +func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } // ObjectValue creates a Value of TypeObject wrapping a nested Section. // // value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) -func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} } +func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors @@ -147,29 +147,29 @@ func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} // Uint64ArrayValue creates a typed array of uint64 values. // // value := Uint64ArrayValue([]uint64{1, 2, 3}) -func Uint64ArrayValue(vs []uint64) Value { - return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs} +func Uint64ArrayValue(values []uint64) Value { + return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } // Uint32ArrayValue creates a typed array of uint32 values. // // value := Uint32ArrayValue([]uint32{1, 2, 3}) -func Uint32ArrayValue(vs []uint32) Value { - return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs} +func Uint32ArrayValue(values []uint32) Value { + return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } // StringArrayValue creates a typed array of byte-string values. // // value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) -func StringArrayValue(vs [][]byte) Value { - return Value{Type: ArrayFlag | TypeString, stringArray: vs} +func StringArrayValue(values [][]byte) Value { + return Value{Type: ArrayFlag | TypeString, stringArray: values} } // ObjectArrayValue creates a typed array of Section values. // // value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) -func ObjectArrayValue(vs []Section) Value { - return Value{Type: ArrayFlag | TypeObject, objectArray: vs} +func ObjectArrayValue(values []Section) Value { + return Value{Type: ArrayFlag | TypeObject, objectArray: values} } // --------------------------------------------------------------------------- @@ -317,25 +317,25 @@ func (v Value) AsSectionArray() ([]Section, error) { // deterministic output. // // data, err := EncodeStorage(section) -func EncodeStorage(s Section) ([]byte, error) { - buf := make([]byte, 0, 256) +func EncodeStorage(section Section) ([]byte, error) { + buffer := make([]byte, 0, 256) // 9-byte storage header. var hdr [StorageHeaderSize]byte binary.LittleEndian.PutUint32(hdr[0:4], StorageSignatureA) binary.LittleEndian.PutUint32(hdr[4:8], StorageSignatureB) hdr[8] = StorageVersion - buf = append(buf, hdr[:]...) + buffer = append(buffer, hdr[:]...) // Encode root section. - out, err := encodeSection(buf, s) + out, err := encodeSection(buffer, section) if err != nil { return nil, err } return out, nil } -// encodeSection appends a section (entry count + entries) to buf. +// encodeSection appends a section (entry count + entries) to buffer. func encodeSection(buf []byte, s Section) ([]byte, error) { // Sort keys for deterministic output. keys := slices.Sorted(maps.Keys(s)) @@ -510,7 +510,7 @@ func DecodeStorage(data []byte) (Section, error) { return s, err } -// decodeSection reads a section from buf and returns the section plus +// decodeSection reads a section from buffer and returns the section plus // the number of bytes consumed. func decodeSection(buf []byte) (Section, int, error) { count, n, err := UnpackVarint(buf) @@ -556,7 +556,7 @@ func decodeSection(buf []byte) (Section, int, error) { return s, off, nil } -// decodeValue reads a value of the given type tag from buf and returns +// decodeValue reads a value of the given type tag from buffer and returns // the value plus bytes consumed. func decodeValue(buf []byte, tag uint8) (Value, int, error) { // Array types. @@ -656,7 +656,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { } } -// decodeArray reads a typed array from buf (tag has ArrayFlag set). +// decodeArray reads a typed array from buffer (tag has ArrayFlag set). func decodeArray(buf []byte, tag uint8) (Value, int, error) { elemType := tag & ^ArrayFlag diff --git a/node/levin/varint.go b/node/levin/varint.go index 5160646..2d26512 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,67 +28,67 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes v using the epee portable-storage varint scheme. +// PackVarint encodes value using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; // the remaining bits carry the value in little-endian order. // // encoded := PackVarint(42) -func PackVarint(v uint64) []byte { +func PackVarint(value uint64) []byte { switch { - case v <= varintMax1: - return []byte{byte((v << 2) | varintMark1)} - case v <= varintMax2: - raw := uint16((v << 2) | varintMark2) - buf := make([]byte, 2) - binary.LittleEndian.PutUint16(buf, raw) - return buf - case v <= varintMax4: - raw := uint32((v << 2) | varintMark4) - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, raw) - return buf + case value <= varintMax1: + return []byte{byte((value << 2) | varintMark1)} + case value <= varintMax2: + raw := uint16((value << 2) | varintMark2) + buffer := make([]byte, 2) + binary.LittleEndian.PutUint16(buffer, raw) + return buffer + case value <= varintMax4: + raw := uint32((value << 2) | varintMark4) + buffer := make([]byte, 4) + binary.LittleEndian.PutUint32(buffer, raw) + return buffer default: - raw := (v << 2) | varintMark8 - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, raw) - return buf + raw := (value << 2) | varintMark8 + buffer := make([]byte, 8) + binary.LittleEndian.PutUint64(buffer, raw) + return buffer } } -// UnpackVarint decodes one epee portable-storage varint from buf. +// UnpackVarint decodes one epee portable-storage varint from buffer. // It returns the decoded value, the number of bytes consumed, and any error. // -// value, err := UnpackVarint(data) -func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { - if len(buf) == 0 { +// value, err := UnpackVarint(buffer) +func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { + if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated } - mark := buf[0] & varintMask + mark := buffer[0] & varintMask switch mark { case varintMark1: - value = uint64(buf[0]) >> 2 + value = uint64(buffer[0]) >> 2 return value, 1, nil case varintMark2: - if len(buf) < 2 { + if len(buffer) < 2 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint16(buf[:2]) + raw := binary.LittleEndian.Uint16(buffer[:2]) value = uint64(raw) >> 2 return value, 2, nil case varintMark4: - if len(buf) < 4 { + if len(buffer) < 4 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint32(buf[:4]) + raw := binary.LittleEndian.Uint32(buffer[:4]) value = uint64(raw) >> 2 return value, 4, nil case varintMark8: - if len(buf) < 8 { + if len(buffer) < 8 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint64(buf[:8]) + raw := binary.LittleEndian.Uint64(buffer[:8]) value = raw >> 2 return value, 8, nil default: diff --git a/node/transport.go b/node/transport.go index 155d900..f580073 100644 --- a/node/transport.go +++ b/node/transport.go @@ -41,7 +41,7 @@ const ( // TransportConfig configures the WebSocket transport. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -55,7 +55,7 @@ type TransportConfig struct { // DefaultTransportConfig returns sensible defaults. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -219,13 +219,13 @@ type PeerConnection struct { } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) -func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { +func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) return &Transport{ config: config, - nodeManager: node, - peerRegistry: registry, + nodeManager: nodeManager, + peerRegistry: peerRegistry, connections: make(map[string]*PeerConnection), messageDeduplicator: NewMessageDeduplicator(5 * time.Minute), // 5 minute TTL for dedup upgrader: websocket.Upgrader{ @@ -405,7 +405,7 @@ func (t *Transport) OnMessage(handler MessageHandler) { t.messageHandler = handler } -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// peerConnection, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -426,7 +426,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return nil, core.E("Transport.Connect", "failed to connect to peer", err) } - pc := &PeerConnection{ + peerConnection := &PeerConnection{ Peer: peer, Conn: conn, LastActivity: time.Now(), @@ -435,63 +435,63 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } - // Perform handshake with challenge-response authentication - // This also derives and stores the shared secret in pc.SharedSecret - if err := t.performHandshake(pc); err != nil { + // Perform handshake with challenge-response authentication. + // This also derives and stores the shared secret in peerConnection.SharedSecret. + if err := t.performHandshake(peerConnection); err != nil { conn.Close() return nil, core.E("Transport.Connect", "handshake failed", err) } // Store connection using the real peer ID from handshake t.mutex.Lock() - t.connections[pc.Peer.ID] = pc + t.connections[peerConnection.Peer.ID] = peerConnection t.mutex.Unlock() - logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) + logging.Debug("connected to peer", logging.Fields{"peer_id": peerConnection.Peer.ID, "secret_len": len(peerConnection.SharedSecret)}) logging.Debug("connected peer metadata", logging.Fields{ - "peer_id": pc.Peer.ID, - "user_agent": pc.UserAgent, + "peer_id": peerConnection.Peer.ID, + "user_agent": peerConnection.UserAgent, }) // Update registry - t.peerRegistry.SetConnected(pc.Peer.ID, true) + t.peerRegistry.SetConnected(peerConnection.Peer.ID, true) // Start read loop t.waitGroup.Add(1) - go t.readLoop(pc) + go t.readLoop(peerConnection) - logging.Debug("started readLoop for peer", logging.Fields{"peer_id": pc.Peer.ID}) + logging.Debug("started readLoop for peer", logging.Fields{"peer_id": peerConnection.Peer.ID}) // Start keepalive t.waitGroup.Add(1) - go t.keepalive(pc) + go t.keepalive(peerConnection) - return pc, nil + return peerConnection, nil } // err := transport.Send("worker-1", message) -func (t *Transport) Send(peerID string, msg *Message) error { +func (t *Transport) Send(peerID string, message *Message) error { t.mutex.RLock() - pc, exists := t.connections[peerID] + peerConnection, exists := t.connections[peerID] t.mutex.RUnlock() if !exists { return core.E("Transport.Send", "peer "+peerID+" not connected", nil) } - return pc.Send(msg) + return peerConnection.Send(message) } -// for pc := range transport.Connections() { -// _ = pc +// for peerConnection := range transport.Connections() { +// _ = peerConnection // } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() defer t.mutex.RUnlock() - for _, pc := range t.connections { - if !yield(pc) { + for _, peerConnection := range t.connections { + if !yield(peerConnection) { return } } @@ -499,15 +499,15 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } // err := transport.Broadcast(announcement) -func (t *Transport) Broadcast(msg *Message) error { +func (t *Transport) Broadcast(message *Message) error { conns := slices.Collect(t.Connections()) var lastErr error - for _, pc := range conns { - if pc.Peer != nil && pc.Peer.ID == msg.From { + for _, peerConnection := range conns { + if peerConnection.Peer != nil && peerConnection.Peer.ID == message.From { continue } - if err := pc.Send(msg); err != nil { + if err := peerConnection.Send(message); err != nil { lastErr = err } } diff --git a/ueps/packet.go b/ueps/packet.go index f65a7b4..ad50f9c 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -51,60 +51,60 @@ func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { // frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { - buf := new(bytes.Buffer) + buffer := new(bytes.Buffer) - if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { + if err := writeTLV(buffer, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } - if err := writeTLV(buf, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { + if err := writeTLV(buffer, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { + if err := writeTLV(buffer, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagIntent, []byte{p.Header.IntentID}); err != nil { + if err := writeTLV(buffer, TagIntent, []byte{p.Header.IntentID}); err != nil { return nil, err } - tsBuf := make([]byte, 2) - binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) - if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { + threatScoreBytes := make([]byte, 2) + binary.BigEndian.PutUint16(threatScoreBytes, p.Header.ThreatScore) + if err := writeTLV(buffer, TagThreatScore, threatScoreBytes); err != nil { return nil, err } mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) + mac.Write(buffer.Bytes()) mac.Write(p.Payload) signature := mac.Sum(nil) - if err := writeTLV(buf, TagHMAC, signature); err != nil { + if err := writeTLV(buffer, TagHMAC, signature); err != nil { return nil, err } - if err := writeTLV(buf, TagPayload, p.Payload); err != nil { + if err := writeTLV(buffer, TagPayload, p.Payload); err != nil { return nil, err } - return buf.Bytes(), nil + return buffer.Bytes(), nil } -// writeTLV(&buf, TagPayload, []byte("hello")) -func writeTLV(w io.Writer, tag uint8, value []byte) error { +// writeTLV(&buffer, TagPayload, []byte("hello")) +func writeTLV(writer io.Writer, tag uint8, value []byte) error { if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } - if _, err := w.Write([]byte{tag}); err != nil { + if _, err := writer.Write([]byte{tag}); err != nil { return err } lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(len(value))) - if _, err := w.Write(lenBuf); err != nil { + if _, err := writer.Write(lenBuf); err != nil { return err } - if _, err := w.Write(value); err != nil { + if _, err := writer.Write(value); err != nil { return err } return nil -- 2.45.3 From a1d9b08bafef79263a054291e00c475cb6d24baf Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:04:41 +0000 Subject: [PATCH 55/60] refactor(node): tighten AX comments across public APIs Co-Authored-By: Virgil --- node/bundle.go | 36 +++-------- node/controller.go | 36 +++-------- node/dispatcher.go | 13 ++-- node/identity.go | 47 ++++----------- node/levin/header.go | 13 +--- node/levin/storage.go | 117 +++++++++++------------------------- node/levin/varint.go | 11 +--- node/message.go | 82 +++++++------------------ node/peer.go | 136 ++++++++++++++---------------------------- node/protocol.go | 24 ++------ node/transport.go | 29 +++------ 11 files changed, 156 insertions(+), 388 deletions(-) diff --git a/node/bundle.go b/node/bundle.go index de52eec..f1c7342 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -14,9 +14,7 @@ import ( "forge.lthn.ai/Snider/Borg/pkg/tim" ) -// BundleType defines the type of deployment bundle. -// -// bundleType := BundleProfile +// bundleType := BundleProfile type BundleType string const ( @@ -28,9 +26,7 @@ const ( BundleFull BundleType = "full" ) -// Bundle represents a deployment bundle for P2P transfer. -// -// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} +// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} type Bundle struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -38,9 +34,7 @@ type Bundle struct { Checksum string `json:"checksum"` // SHA-256 of Data } -// BundleManifest describes the contents of a bundle. -// -// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} +// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} type BundleManifest struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -50,9 +44,7 @@ type BundleManifest struct { CreatedAt string `json:"createdAt"` } -// CreateProfileBundle creates an encrypted bundle containing a mining profile. -// -// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") +// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config timBundle, err := tim.New() @@ -78,9 +70,7 @@ func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bun }, nil } -// CreateProfileBundleUnencrypted creates a plain JSON bundle (for testing or trusted networks). -// -// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") +// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error) { checksum := calculateChecksum(profileJSON) @@ -92,9 +82,7 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e }, nil } -// CreateMinerBundle creates an encrypted bundle containing a miner binary and optional profile. -// -// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") +// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary minerContent, err := filesystemRead(minerPath) @@ -144,9 +132,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo }, nil } -// ExtractProfileBundle decrypts and extracts a profile bundle. -// -// profileJSON, err := ExtractProfileBundle(bundle, "password") +// profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -167,9 +153,7 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { return timBundle.Config, nil } -// ExtractMinerBundle decrypts and extracts a miner bundle, returning the miner path and profile. -// -// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") +// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -197,9 +181,7 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string return minerPath, timBundle.Config, nil } -// VerifyBundle checks if a bundle's checksum is valid. -// -// ok := VerifyBundle(bundle) +// ok := VerifyBundle(bundle) func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } diff --git a/node/controller.go b/node/controller.go index 3d4ebc9..17bbc22 100644 --- a/node/controller.go +++ b/node/controller.go @@ -21,9 +21,7 @@ type Controller struct { pendingRequests map[string]chan *Message // message ID -> response channel } -// NewController wires a controller to a node manager, peer registry, and transport. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transport *Transport) *Controller { c := &Controller{ nodeManager: nodeManager, @@ -117,9 +115,7 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D } } -// GetRemoteStats requests miner statistics from a remote peer. -// -// stats, err := controller.GetRemoteStats("worker-1") +// stats, err := controller.GetRemoteStats("worker-1") func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -144,9 +140,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } -// StartRemoteMiner requests a remote peer to start a miner with a given profile. -// -// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) +// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -185,9 +179,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi return nil } -// StopRemoteMiner requests a remote peer to stop a miner. -// -// err := controller.StopRemoteMiner("worker-1", "xmrig-0") +// err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -220,9 +212,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return nil } -// GetRemoteLogs requests console logs from a remote miner. -// -// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) +// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -252,9 +242,7 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// GetAllStats fetches stats from all connected peers. -// -// statsByPeerID := controller.GetAllStats() +// statsByPeerID := controller.GetAllStats() func (c *Controller) GetAllStats() map[string]*StatsPayload { results := make(map[string]*StatsPayload) var mu sync.Mutex @@ -283,9 +271,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// PingPeer sends a ping to a peer and refreshes that peer's metrics. -// -// rttMilliseconds, err := controller.PingPeer("worker-1") +// rttMilliseconds, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -323,9 +309,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return rtt, nil } -// ConnectToPeer opens a transport connection to a peer. -// -// err := controller.ConnectToPeer("worker-1") +// err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { peer := c.peerRegistry.GetPeer(peerID) if peer == nil { @@ -336,9 +320,7 @@ func (c *Controller) ConnectToPeer(peerID string) error { return err } -// DisconnectFromPeer closes the active connection to a peer. -// -// err := controller.DisconnectFromPeer("worker-1") +// err := controller.DisconnectFromPeer("worker-1") func (c *Controller) DisconnectFromPeer(peerID string) error { conn := c.transport.GetConnection(peerID) if conn == nil { diff --git a/node/dispatcher.go b/node/dispatcher.go index 1307713..faedf4b 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -22,10 +22,7 @@ const ( IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) -// IntentHandler processes a UEPS packet that has been routed by intent. -// Implementations receive the fully parsed and HMAC-verified packet. -// -// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } +// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error // dispatcher := NewDispatcher() @@ -58,10 +55,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// for intentID, handler := range dispatcher.Handlers() { -// _ = intentID -// _ = handler -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() diff --git a/node/identity.go b/node/identity.go index 1309281..ae5d1f1 100644 --- a/node/identity.go +++ b/node/identity.go @@ -16,12 +16,10 @@ import ( "github.com/adrg/xdg" ) -// ChallengeSize is the size of the challenge in bytes +// challenge := make([]byte, ChallengeSize) const ChallengeSize = 32 -// GenerateChallenge creates a random challenge for authentication. -// -// challenge, err := GenerateChallenge() +// challenge, err := GenerateChallenge() func GenerateChallenge() ([]byte, error) { challenge := make([]byte, ChallengeSize) if _, err := rand.Read(challenge); err != nil { @@ -30,27 +28,20 @@ func GenerateChallenge() ([]byte, error) { return challenge, nil } -// SignChallenge creates an HMAC signature of a challenge using a shared secret. -// The signature proves possession of the shared secret without revealing it. -// -// signature := SignChallenge(challenge, sharedSecret) +// signature := SignChallenge(challenge, sharedSecret) func SignChallenge(challenge []byte, sharedSecret []byte) []byte { mac := hmac.New(sha256.New, sharedSecret) mac.Write(challenge) return mac.Sum(nil) } -// VerifyChallenge verifies that a challenge response was signed with the correct shared secret. -// -// ok := VerifyChallenge(challenge, signature, sharedSecret) +// ok := VerifyChallenge(challenge, signature, sharedSecret) func VerifyChallenge(challenge, response, sharedSecret []byte) bool { expected := SignChallenge(challenge, sharedSecret) return hmac.Equal(response, expected) } -// NodeRole defines the operational mode of a node. -// -// role := RoleWorker +// role := RoleWorker type NodeRole string const ( @@ -62,9 +53,7 @@ const ( RoleDual NodeRole = "dual" ) -// NodeIdentity represents the public identity of a node. -// -// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} +// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} type NodeIdentity struct { ID string `json:"id"` // Derived from public key (first 16 bytes hex) Name string `json:"name"` // Human-friendly name @@ -83,9 +72,7 @@ type NodeManager struct { mu sync.RWMutex } -// NewNodeManager loads the default node identity store. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() func NewNodeManager() (*NodeManager, error) { keyPath, err := xdg.DataFile("lethean-desktop/node/private.key") if err != nil { @@ -100,12 +87,10 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// NewNodeManagerFromPaths loads or creates a node identity store at explicit paths. -// // Missing files are treated as a fresh install; malformed or partial identity // state is returned as an error so callers can handle it explicitly. // -// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, @@ -131,9 +116,7 @@ func (n *NodeManager) HasIdentity() bool { return n.identity != nil } -// GetIdentity returns a copy of the loaded node identity. -// -// identity := nodeManager.GetIdentity() +// identity := nodeManager.GetIdentity() func (n *NodeManager) GetIdentity() *NodeIdentity { n.mu.RLock() defer n.mu.RUnlock() @@ -145,9 +128,7 @@ func (n *NodeManager) GetIdentity() *NodeIdentity { return &identity } -// GenerateIdentity writes a new node identity for the given name and role. -// -// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) +// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { n.mu.Lock() defer n.mu.Unlock() @@ -187,9 +168,7 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { return nil } -// DeriveSharedSecret hashes an X25519 shared secret for use as a symmetric key. -// -// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) +// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error) { n.mu.RLock() defer n.mu.RUnlock() @@ -292,9 +271,7 @@ func (n *NodeManager) loadIdentity() error { return nil } -// Delete removes the node identity and key files from disk. -// -// err := nodeManager.Delete() +// err := nodeManager.Delete() func (n *NodeManager) Delete() error { n.mu.Lock() defer n.mu.Unlock() diff --git a/node/levin/header.go b/node/levin/header.go index ee4d9ab..911435a 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -47,9 +47,7 @@ var ( ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) -// Header is the 33-byte packed header that prefixes every Levin message. -// -// header := Header{Command: CommandHandshake, ExpectResponse: true} +// header := Header{Command: CommandHandshake, ExpectResponse: true} type Header struct { Signature uint64 PayloadSize uint64 @@ -60,9 +58,7 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). -// -// encoded := EncodeHeader(header) +// encoded := EncodeHeader(header) func EncodeHeader(header *Header) [HeaderSize]byte { var headerBytes [HeaderSize]byte binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) @@ -79,10 +75,7 @@ func EncodeHeader(header *Header) [HeaderSize]byte { return headerBytes } -// DecodeHeader deserialises a 33-byte array into a Header, validating -// the magic signature. -// -// header, err := DecodeHeader(headerBytes) +// header, err := DecodeHeader(headerBytes) func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { var header Header header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) diff --git a/node/levin/storage.go b/node/levin/storage.go index 9a6d4ed..08aa7ca 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -48,16 +48,10 @@ var ( ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) ) -// Section is an ordered map of named values forming a portable storage section. -// Field iteration order is always alphabetical by key for deterministic encoding. -// -// section := Section{"id": StringValue([]byte("peer-1"))} +// section := Section{"id": StringValue([]byte("peer-1"))} type Section map[string]Value -// Value holds a typed portable storage value. Use the constructor functions -// (Uint64Value, StringValue, ObjectValue, etc.) to create instances. -// -// value := StringValue([]byte("peer-1")) +// value := StringValue([]byte("peer-1")) type Value struct { Type uint8 @@ -80,94 +74,62 @@ type Value struct { // Scalar constructors // --------------------------------------------------------------------------- -// Uint64Value creates a Value of TypeUint64. -// -// value := Uint64Value(42) +// value := Uint64Value(42) func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } -// Uint32Value creates a Value of TypeUint32. -// -// value := Uint32Value(42) +// value := Uint32Value(42) func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } -// Uint16Value creates a Value of TypeUint16. -// -// value := Uint16Value(42) +// value := Uint16Value(42) func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } -// Uint8Value creates a Value of TypeUint8. -// -// value := Uint8Value(42) +// value := Uint8Value(42) func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } -// Int64Value creates a Value of TypeInt64. -// -// value := Int64Value(42) +// value := Int64Value(42) func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } -// Int32Value creates a Value of TypeInt32. -// -// value := Int32Value(42) +// value := Int32Value(42) func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } -// Int16Value creates a Value of TypeInt16. -// -// value := Int16Value(42) +// value := Int16Value(42) func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } -// Int8Value creates a Value of TypeInt8. -// -// value := Int8Value(42) +// value := Int8Value(42) func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } -// BoolValue creates a Value of TypeBool. -// -// value := BoolValue(true) +// value := BoolValue(true) func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } -// DoubleValue creates a Value of TypeDouble. -// -// value := DoubleValue(3.14) +// value := DoubleValue(3.14) func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } -// StringValue creates a Value of TypeString. The slice is not copied. -// -// value := StringValue([]byte("hello")) +// value := StringValue([]byte("hello")) func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } -// ObjectValue creates a Value of TypeObject wrapping a nested Section. -// -// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) +// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors // --------------------------------------------------------------------------- -// Uint64ArrayValue creates a typed array of uint64 values. -// -// value := Uint64ArrayValue([]uint64{1, 2, 3}) +// value := Uint64ArrayValue([]uint64{1, 2, 3}) func Uint64ArrayValue(values []uint64) Value { return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } -// Uint32ArrayValue creates a typed array of uint32 values. -// -// value := Uint32ArrayValue([]uint32{1, 2, 3}) +// value := Uint32ArrayValue([]uint32{1, 2, 3}) func Uint32ArrayValue(values []uint32) Value { return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } -// StringArrayValue creates a typed array of byte-string values. -// -// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) +// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) func StringArrayValue(values [][]byte) Value { return Value{Type: ArrayFlag | TypeString, stringArray: values} } -// ObjectArrayValue creates a typed array of Section values. -// -// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) +// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) func ObjectArrayValue(values []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: values} } @@ -176,7 +138,7 @@ func ObjectArrayValue(values []Section) Value { // Scalar accessors // --------------------------------------------------------------------------- -// AsUint64 returns the uint64 value or an error on type mismatch. +// value, err := Uint64Value(42).AsUint64() func (v Value) AsUint64() (uint64, error) { if v.Type != TypeUint64 { return 0, ErrorStorageTypeMismatch @@ -184,7 +146,7 @@ func (v Value) AsUint64() (uint64, error) { return v.uintVal, nil } -// AsUint32 returns the uint32 value or an error on type mismatch. +// value, err := Uint32Value(42).AsUint32() func (v Value) AsUint32() (uint32, error) { if v.Type != TypeUint32 { return 0, ErrorStorageTypeMismatch @@ -192,7 +154,7 @@ func (v Value) AsUint32() (uint32, error) { return uint32(v.uintVal), nil } -// AsUint16 returns the uint16 value or an error on type mismatch. +// value, err := Uint16Value(42).AsUint16() func (v Value) AsUint16() (uint16, error) { if v.Type != TypeUint16 { return 0, ErrorStorageTypeMismatch @@ -200,7 +162,7 @@ func (v Value) AsUint16() (uint16, error) { return uint16(v.uintVal), nil } -// AsUint8 returns the uint8 value or an error on type mismatch. +// value, err := Uint8Value(42).AsUint8() func (v Value) AsUint8() (uint8, error) { if v.Type != TypeUint8 { return 0, ErrorStorageTypeMismatch @@ -208,7 +170,7 @@ func (v Value) AsUint8() (uint8, error) { return uint8(v.uintVal), nil } -// AsInt64 returns the int64 value or an error on type mismatch. +// value, err := Int64Value(42).AsInt64() func (v Value) AsInt64() (int64, error) { if v.Type != TypeInt64 { return 0, ErrorStorageTypeMismatch @@ -216,7 +178,7 @@ func (v Value) AsInt64() (int64, error) { return v.intVal, nil } -// AsInt32 returns the int32 value or an error on type mismatch. +// value, err := Int32Value(42).AsInt32() func (v Value) AsInt32() (int32, error) { if v.Type != TypeInt32 { return 0, ErrorStorageTypeMismatch @@ -224,7 +186,7 @@ func (v Value) AsInt32() (int32, error) { return int32(v.intVal), nil } -// AsInt16 returns the int16 value or an error on type mismatch. +// value, err := Int16Value(42).AsInt16() func (v Value) AsInt16() (int16, error) { if v.Type != TypeInt16 { return 0, ErrorStorageTypeMismatch @@ -232,7 +194,7 @@ func (v Value) AsInt16() (int16, error) { return int16(v.intVal), nil } -// AsInt8 returns the int8 value or an error on type mismatch. +// value, err := Int8Value(42).AsInt8() func (v Value) AsInt8() (int8, error) { if v.Type != TypeInt8 { return 0, ErrorStorageTypeMismatch @@ -240,7 +202,7 @@ func (v Value) AsInt8() (int8, error) { return int8(v.intVal), nil } -// AsBool returns the bool value or an error on type mismatch. +// value, err := BoolValue(true).AsBool() func (v Value) AsBool() (bool, error) { if v.Type != TypeBool { return false, ErrorStorageTypeMismatch @@ -248,7 +210,7 @@ func (v Value) AsBool() (bool, error) { return v.boolVal, nil } -// AsDouble returns the float64 value or an error on type mismatch. +// value, err := DoubleValue(3.14).AsDouble() func (v Value) AsDouble() (float64, error) { if v.Type != TypeDouble { return 0, ErrorStorageTypeMismatch @@ -256,7 +218,7 @@ func (v Value) AsDouble() (float64, error) { return v.floatVal, nil } -// AsString returns the byte-string value or an error on type mismatch. +// value, err := StringValue([]byte("hello")).AsString() func (v Value) AsString() ([]byte, error) { if v.Type != TypeString { return nil, ErrorStorageTypeMismatch @@ -264,7 +226,7 @@ func (v Value) AsString() ([]byte, error) { return v.bytesVal, nil } -// AsSection returns the nested Section or an error on type mismatch. +// section, err := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}).AsSection() func (v Value) AsSection() (Section, error) { if v.Type != TypeObject { return nil, ErrorStorageTypeMismatch @@ -276,7 +238,7 @@ func (v Value) AsSection() (Section, error) { // Array accessors // --------------------------------------------------------------------------- -// AsUint64Array returns the []uint64 array or an error on type mismatch. +// values, err := Uint64ArrayValue([]uint64{1, 2, 3}).AsUint64Array() func (v Value) AsUint64Array() ([]uint64, error) { if v.Type != (ArrayFlag | TypeUint64) { return nil, ErrorStorageTypeMismatch @@ -284,7 +246,7 @@ func (v Value) AsUint64Array() ([]uint64, error) { return v.uint64Array, nil } -// AsUint32Array returns the []uint32 array or an error on type mismatch. +// values, err := Uint32ArrayValue([]uint32{1, 2, 3}).AsUint32Array() func (v Value) AsUint32Array() ([]uint32, error) { if v.Type != (ArrayFlag | TypeUint32) { return nil, ErrorStorageTypeMismatch @@ -292,7 +254,7 @@ func (v Value) AsUint32Array() ([]uint32, error) { return v.uint32Array, nil } -// AsStringArray returns the [][]byte array or an error on type mismatch. +// values, err := StringArrayValue([][]byte{[]byte("a"), []byte("b")}).AsStringArray() func (v Value) AsStringArray() ([][]byte, error) { if v.Type != (ArrayFlag | TypeString) { return nil, ErrorStorageTypeMismatch @@ -300,7 +262,7 @@ func (v Value) AsStringArray() ([][]byte, error) { return v.stringArray, nil } -// AsSectionArray returns the []Section array or an error on type mismatch. +// values, err := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}).AsSectionArray() func (v Value) AsSectionArray() ([]Section, error) { if v.Type != (ArrayFlag | TypeObject) { return nil, ErrorStorageTypeMismatch @@ -312,11 +274,7 @@ func (v Value) AsSectionArray() ([]Section, error) { // Encoder // --------------------------------------------------------------------------- -// EncodeStorage serialises a Section to the portable storage binary format, -// including the 9-byte header. Keys are sorted alphabetically to ensure -// deterministic output. -// -// data, err := EncodeStorage(section) +// data, err := EncodeStorage(section) func EncodeStorage(section Section) ([]byte, error) { buffer := make([]byte, 0, 256) @@ -486,10 +444,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { // Decoder // --------------------------------------------------------------------------- -// DecodeStorage deserialises portable storage binary data (including the -// 9-byte header) into a Section. -// -// section, err := DecodeStorage(data) +// section, err := DecodeStorage(data) func DecodeStorage(data []byte) (Section, error) { if len(data) < StorageHeaderSize { return nil, ErrorStorageTruncated diff --git a/node/levin/varint.go b/node/levin/varint.go index 2d26512..b245833 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,11 +28,7 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes value using the epee portable-storage varint scheme. -// The low two bits of the first byte indicate the total encoded width; -// the remaining bits carry the value in little-endian order. -// -// encoded := PackVarint(42) +// encoded := PackVarint(42) func PackVarint(value uint64) []byte { switch { case value <= varintMax1: @@ -55,10 +51,7 @@ func PackVarint(value uint64) []byte { } } -// UnpackVarint decodes one epee portable-storage varint from buffer. -// It returns the decoded value, the number of bytes consumed, and any error. -// -// value, err := UnpackVarint(buffer) +// value, err := UnpackVarint(buffer) func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated diff --git a/node/message.go b/node/message.go index 1640fde..eb21c41 100644 --- a/node/message.go +++ b/node/message.go @@ -22,14 +22,10 @@ const ( // versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} -// RawMessage stores an already-encoded JSON payload for deferred decoding. -// -// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) +// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) type RawMessage []byte -// MarshalJSON preserves the raw JSON payload when the message is encoded. -// -// data, err := RawMessage(`{"ok":true}`).MarshalJSON() +// data, err := RawMessage(`{"ok":true}`).MarshalJSON() func (m RawMessage) MarshalJSON() ([]byte, error) { if m == nil { return []byte("null"), nil @@ -38,10 +34,8 @@ func (m RawMessage) MarshalJSON() ([]byte, error) { return m, nil } -// UnmarshalJSON stores the raw JSON payload bytes without decoding them. -// -// var payload RawMessage -// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) +// var payload RawMessage +// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) func (m *RawMessage) UnmarshalJSON(data []byte) error { if m == nil { return core.E("node.RawMessage.UnmarshalJSON", "raw message target is nil", nil) @@ -51,9 +45,7 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error { return nil } -// IsProtocolVersionSupported checks if a given version is supported. -// -// ok := IsProtocolVersionSupported("1.0") +// ok := IsProtocolVersionSupported("1.0") func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } @@ -90,9 +82,7 @@ const ( MessageError MessageType = "error" ) -// Message represents a P2P message between nodes. -// -// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) +// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -103,9 +93,7 @@ type Message struct { ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to } -// NewMessage builds a message with a generated ID and timestamp. -// -// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) +// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) func NewMessage(messageType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -151,18 +139,14 @@ func (m *Message) ParsePayload(target any) error { // --- Payload Types --- -// HandshakePayload is sent during connection establishment. -// -// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} +// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} type HandshakePayload struct { Identity NodeIdentity `json:"identity"` Challenge []byte `json:"challenge,omitempty"` // Random bytes for auth Version string `json:"version"` // Protocol version } -// HandshakeAckPayload is the response to a handshake. -// -// ack := HandshakeAckPayload{Accepted: true} +// ack := HandshakeAckPayload{Accepted: true} type HandshakeAckPayload struct { Identity NodeIdentity `json:"identity"` ChallengeResponse []byte `json:"challengeResponse,omitempty"` @@ -170,49 +154,37 @@ type HandshakeAckPayload struct { Reason string `json:"reason,omitempty"` // If not accepted } -// PingPayload for keepalive/latency measurement. -// -// payload := PingPayload{SentAt: 42} +// payload := PingPayload{SentAt: 42} type PingPayload struct { SentAt int64 `json:"sentAt"` // Unix timestamp in milliseconds } -// PongPayload response to ping. -// -// payload := PongPayload{SentAt: 42, ReceivedAt: 43} +// payload := PongPayload{SentAt: 42, ReceivedAt: 43} type PongPayload struct { SentAt int64 `json:"sentAt"` // Echo of ping's sentAt ReceivedAt int64 `json:"receivedAt"` // When ping was received } -// StartMinerPayload requests starting a miner. -// -// payload := StartMinerPayload{MinerType: "xmrig"} +// payload := StartMinerPayload{MinerType: "xmrig"} type StartMinerPayload struct { MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") ProfileID string `json:"profileId,omitempty"` Config RawMessage `json:"config,omitempty"` // Override profile config } -// StopMinerPayload requests stopping a miner. -// -// payload := StopMinerPayload{MinerName: "xmrig-0"} +// payload := StopMinerPayload{MinerName: "xmrig-0"} type StopMinerPayload struct { MinerName string `json:"minerName"` } -// MinerAckPayload acknowledges a miner start/stop operation. -// -// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} +// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} type MinerAckPayload struct { Success bool `json:"success"` MinerName string `json:"minerName,omitempty"` Error string `json:"error,omitempty"` } -// MinerStatsItem represents stats for a single miner. -// -// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} +// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} type MinerStatsItem struct { Name string `json:"name"` Type string `json:"type"` @@ -225,9 +197,7 @@ type MinerStatsItem struct { CPUThreads int `json:"cpuThreads,omitempty"` } -// StatsPayload contains miner statistics. -// -// stats := StatsPayload{NodeID: "worker-1"} +// stats := StatsPayload{NodeID: "worker-1"} type StatsPayload struct { NodeID string `json:"nodeId"` NodeName string `json:"nodeName"` @@ -235,27 +205,21 @@ type StatsPayload struct { Uptime int64 `json:"uptime"` // Node uptime in seconds } -// LogsRequestPayload requests console logs from a miner. -// -// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} +// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} type LogsRequestPayload struct { MinerName string `json:"minerName"` Lines int `json:"lines"` // Number of lines to fetch Since int64 `json:"since,omitempty"` // Unix timestamp, logs after this time } -// LogsPayload contains console log lines. -// -// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} +// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} type LogsPayload struct { MinerName string `json:"minerName"` Lines []string `json:"lines"` HasMore bool `json:"hasMore"` // More logs available } -// DeployPayload contains a deployment bundle. -// -// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} +// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} type DeployPayload struct { BundleType string `json:"type"` // "profile" | "miner" | "full" Data []byte `json:"data"` // STIM-encrypted bundle @@ -263,18 +227,14 @@ type DeployPayload struct { Name string `json:"name"` // Profile or miner name } -// DeployAckPayload acknowledges a deployment. -// -// ack := DeployAckPayload{Success: true, Name: "xmrig"} +// ack := DeployAckPayload{Success: true, Name: "xmrig"} type DeployAckPayload struct { Success bool `json:"success"` Name string `json:"name,omitempty"` Error string `json:"error,omitempty"` } -// ErrorPayload contains error information. -// -// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} +// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} type ErrorPayload struct { Code int `json:"code"` Message string `json:"message"` diff --git a/node/peer.go b/node/peer.go index b44348a..42f8801 100644 --- a/node/peer.go +++ b/node/peer.go @@ -15,16 +15,14 @@ import ( "github.com/adrg/xdg" ) -// Peer represents a known remote node. -// // peer := &Peer{ -// ID: "worker-1", -// Name: "Worker 1", -// Address: "127.0.0.1:9101", -// PingMilliseconds: 42.5, -// GeographicKilometres: 100, -// Score: 80, -// } +// ID: "worker-1", +// Name: "Worker 1", +// Address: "127.0.0.1:9101", +// PingMilliseconds: 42.5, +// GeographicKilometres: 100, +// Score: 80, +// } type Peer struct { ID string `json:"id"` Name string `json:"name"` @@ -47,9 +45,7 @@ type Peer struct { // peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second -// PeerAuthMode controls how unknown peers are handled -// -// mode := PeerAuthAllowlist +// mode := PeerAuthAllowlist type PeerAuthMode int const ( @@ -125,9 +121,7 @@ var ( scoreWeight = 1.2 ) -// NewPeerRegistry loads the default peer registry. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() func NewPeerRegistry() (*PeerRegistry, error) { peersPath, err := xdg.ConfigFile("lethean-desktop/peers.json") if err != nil { @@ -137,12 +131,10 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// NewPeerRegistryFromPath loads or creates a peer registry at an explicit path. -// // Missing files are treated as an empty registry; malformed registry files are // returned as errors so callers can repair the persisted state. // -// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -165,9 +157,7 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } -// SetAuthMode changes how unknown peers are handled. -// -// registry.SetAuthMode(PeerAuthAllowlist) +// registry.SetAuthMode(PeerAuthAllowlist) func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -175,18 +165,14 @@ func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { logging.Info("peer auth mode changed", logging.Fields{"mode": mode}) } -// GetAuthMode returns the current authentication mode. -// -// mode := registry.GetAuthMode() +// mode := registry.GetAuthMode() func (r *PeerRegistry) GetAuthMode() PeerAuthMode { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.authMode } -// AllowPublicKey adds a public key to the allowlist. -// -// registry.AllowPublicKey(peer.PublicKey) +// registry.AllowPublicKey(peer.PublicKey) func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -194,9 +180,7 @@ func (r *PeerRegistry) AllowPublicKey(publicKey string) { logging.Debug("public key added to allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// RevokePublicKey removes a public key from the allowlist. -// -// registry.RevokePublicKey(peer.PublicKey) +// registry.RevokePublicKey(peer.PublicKey) func (r *PeerRegistry) RevokePublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -204,20 +188,17 @@ func (r *PeerRegistry) RevokePublicKey(publicKey string) { logging.Debug("public key removed from allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// IsPublicKeyAllowed checks if a public key is in the allowlist. -// -// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) +// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.allowedPublicKeys[publicKey] } -// IsPeerAllowed checks if a peer is allowed to connect based on auth mode. // Returns true when AuthMode is Open (all allowed), or when Allowlist mode is active // and the peer is pre-registered or its public key is in the allowlist. // -// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) +// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { r.allowedPublicKeyMu.RLock() authMode := r.authMode @@ -242,18 +223,14 @@ func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { return keyAllowed } -// ListAllowedPublicKeys returns all allowlisted public keys. -// -// keys := registry.ListAllowedPublicKeys() +// keys := registry.ListAllowedPublicKeys() func (r *PeerRegistry) ListAllowedPublicKeys() []string { return slices.Collect(r.AllowedPublicKeys()) } -// AllowedPublicKeys returns an iterator over all allowlisted public keys. -// // for key := range registry.AllowedPublicKeys() { -// log.Printf("allowed: %s", key[:16]) -// } +// log.Printf("allowed: %s", key[:16]) +// } func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { return func(yield func(string) bool) { r.allowedPublicKeyMu.RLock() @@ -267,10 +244,9 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// AddPeer adds a new peer to the registry. // Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. // -// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) +// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { r.mu.Lock() @@ -306,10 +282,9 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { return nil } -// UpdatePeer updates an existing peer's information. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) +// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { r.mu.Lock() @@ -326,10 +301,9 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { return nil } -// RemovePeer removes a peer from the registry. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.RemovePeer("worker-1") +// err := registry.RemovePeer("worker-1") func (r *PeerRegistry) RemovePeer(id string) error { r.mu.Lock() @@ -346,9 +320,7 @@ func (r *PeerRegistry) RemovePeer(id string) error { return nil } -// GetPeer returns a copy of the peer with the supplied ID. -// -// peer := registry.GetPeer("worker-1") +// peer := registry.GetPeer("worker-1") func (r *PeerRegistry) GetPeer(id string) *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -363,13 +335,16 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return &peerCopy } -// ListPeers returns all registered peers. +// peers := registry.ListPeers() func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) } -// Peers returns an iterator over all registered peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.Peers() { +// _ = peer +// } func (r *PeerRegistry) Peers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -384,10 +359,7 @@ func (r *PeerRegistry) Peers() iter.Seq[*Peer] { } } -// UpdateMetrics updates a peer's performance metrics. -// -// registry.UpdateMetrics("worker-1", 42.5, 100, 3) -// +// registry.UpdateMetrics("worker-1", 42.5, 100, 3) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilometres float64, hopCount int) error { r.mu.Lock() @@ -410,7 +382,7 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilo return nil } -// UpdateScore updates a peer's reliability score. +// registry.UpdateScore("worker-1", 90) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateScore(id string, score float64) error { r.mu.Lock() @@ -432,9 +404,7 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { return nil } -// SetConnected updates a peer's connection state. -// -// registry.SetConnected("worker-1", true) +// registry.SetConnected("worker-1", true) func (r *PeerRegistry) SetConnected(id string, connected bool) { r.mu.Lock() defer r.mu.Unlock() @@ -457,9 +427,7 @@ const ( ScoreDefault = 50.0 // Default score for new peers ) -// RecordSuccess records a successful interaction with a peer, improving their score. -// -// registry.RecordSuccess("worker-1") +// registry.RecordSuccess("worker-1") func (r *PeerRegistry) RecordSuccess(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -474,9 +442,7 @@ func (r *PeerRegistry) RecordSuccess(id string) { r.scheduleSave() } -// RecordFailure records a failed interaction with a peer, reducing their score. -// -// registry.RecordFailure("worker-1") +// registry.RecordFailure("worker-1") func (r *PeerRegistry) RecordFailure(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -497,9 +463,7 @@ func (r *PeerRegistry) RecordFailure(id string) { }) } -// RecordTimeout records a timeout when communicating with a peer. -// -// registry.RecordTimeout("worker-1") +// registry.RecordTimeout("worker-1") func (r *PeerRegistry) RecordTimeout(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -520,9 +484,7 @@ func (r *PeerRegistry) RecordTimeout(id string) { }) } -// GetPeersByScore returns peers sorted by score, highest first. -// -// peers := registry.GetPeersByScore() +// peers := registry.GetPeersByScore() func (r *PeerRegistry) GetPeersByScore() []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -543,11 +505,9 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return peers } -// PeersByScore returns an iterator over peers sorted by score (highest first). -// // for peer := range registry.PeersByScore() { -// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) -// } +// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) +// } func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { peers := r.GetPeersByScore() @@ -559,10 +519,9 @@ func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { } } -// SelectOptimalPeer returns the best peer based on multi-factor optimisation. // Uses Poindexter KD-tree to find the peer closest to ideal metrics (low ping, low hops, high score). // -// peer := registry.SelectOptimalPeer() +// peer := registry.SelectOptimalPeer() func (r *PeerRegistry) SelectOptimalPeer() *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -589,9 +548,7 @@ func (r *PeerRegistry) SelectOptimalPeer() *Peer { return &peerCopy } -// SelectNearestPeers returns the n best peers based on multi-factor optimisation. -// -// peers := registry.SelectNearestPeers(3) +// peers := registry.SelectNearestPeers(3) func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -616,15 +573,16 @@ func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { return peers } -// GetConnectedPeers returns all currently connected peers as a slice. -// -// connectedPeers := registry.GetConnectedPeers() +// connectedPeers := registry.GetConnectedPeers() func (r *PeerRegistry) GetConnectedPeers() []*Peer { return slices.Collect(r.ConnectedPeers()) } -// ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.ConnectedPeers() { +// _ = peer +// } func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -641,9 +599,7 @@ func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { } } -// Count returns the number of registered peers. -// -// n := registry.Count() +// n := registry.Count() func (r *PeerRegistry) Count() int { r.mu.RLock() defer r.mu.RUnlock() @@ -751,7 +707,7 @@ func (r *PeerRegistry) saveNow() error { return nil } -// Close flushes any pending changes and releases resources. +// registry.Close() func (r *PeerRegistry) Close() error { // Cancel any pending timer and save immediately if changes are queued. r.saveMutex.Lock() diff --git a/node/protocol.go b/node/protocol.go index 72c1624..23d5152 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -4,9 +4,7 @@ import ( core "dappco.re/go/core" ) -// ProtocolError represents an error from the remote peer. -// -// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} +// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} type ProtocolError struct { Code int Message string @@ -16,14 +14,10 @@ func (e *ProtocolError) Error() string { return core.Sprintf("remote error (%d): %s", e.Code, e.Message) } -// ResponseHandler provides helpers for handling protocol responses. -// -// handler := &ResponseHandler{} +// handler := &ResponseHandler{} type ResponseHandler struct{} -// ValidateResponse checks a response against the expected type. -// -// err := handler.ValidateResponse(resp, MessageStats) +// err := handler.ValidateResponse(resp, MessageStats) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { return core.E("ResponseHandler.ValidateResponse", "nil response", nil) @@ -46,9 +40,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy return nil } -// ParseResponse validates the response and parses the payload into the target. -// -// err := handler.ParseResponse(resp, MessageStats, &stats) +// err := handler.ParseResponse(resp, MessageStats, &stats) func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error { if err := h.ValidateResponse(resp, expectedType); err != nil { return err @@ -80,17 +72,13 @@ func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } -// IsProtocolError returns true if the error is a ProtocolError. -// -// ok := IsProtocolError(err) +// ok := IsProtocolError(err) func IsProtocolError(err error) bool { _, ok := err.(*ProtocolError) return ok } -// GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. -// -// code := GetProtocolErrorCode(err) +// code := GetProtocolErrorCode(err) func GetProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code diff --git a/node/transport.go b/node/transport.go index f580073..e2c74e9 100644 --- a/node/transport.go +++ b/node/transport.go @@ -39,9 +39,7 @@ const ( defaultTransportMaximumConnections = 100 ) -// TransportConfig configures the WebSocket transport. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -53,9 +51,7 @@ type TransportConfig struct { PongTimeout time.Duration // Timeout waiting for pong } -// DefaultTransportConfig returns sensible defaults. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -92,23 +88,17 @@ func (c TransportConfig) maximumConnections() int { return defaultTransportMaximumConnections } -// MessageHandler processes incoming messages. -// -// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} +// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} type MessageHandler func(peerConnection *PeerConnection, message *Message) -// MessageDeduplicator tracks recent message IDs to prevent duplicate processing. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) type MessageDeduplicator struct { recentMessageTimes map[string]time.Time mutex sync.RWMutex timeToLive time.Duration } -// NewMessageDeduplicator creates a deduplicator with the supplied retention window. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator { d := &MessageDeduplicator{ recentMessageTimes: make(map[string]time.Time), @@ -170,10 +160,7 @@ type PeerRateLimiter struct { mutex sync.Mutex } -// NewPeerRateLimiter creates a token bucket seeded with maxTokens and refilled -// at refillRate tokens per second. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { return &PeerRateLimiter{ availableTokens: maxTokens, @@ -979,9 +966,7 @@ func (pc *PeerConnection) Close() error { return err } -// DisconnectPayload contains reason for disconnect. -// -// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} +// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} type DisconnectPayload struct { Reason string `json:"reason"` Code int `json:"code"` // Optional disconnect code -- 2.45.3 From e3b66f7e8cad694a21ca2325109f8d5fb466e6f0 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:11:17 +0000 Subject: [PATCH 56/60] refactor(node): align remaining AX naming and examples Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/routing.md | 2 +- docs/ueps.md | 2 +- node/dispatcher.go | 11 +++++------ node/dispatcher_test.go | 12 ++++++------ node/integration_test.go | 2 +- node/levin/header.go | 12 ++++++------ node/message.go | 24 +++++++++--------------- node/protocol.go | 8 ++------ node/transport.go | 20 ++++++++++---------- node/worker.go | 6 ++---- 11 files changed, 44 insertions(+), 57 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 1f9f3a2..bb4b90c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -179,7 +179,7 @@ Auto-connect: if the target peer is not yet connected, `sendRequest` calls `tran |----------|-------|---------| | `IntentHandshake` | `0x01` | Connection establishment | | `IntentCompute` | `0x20` | Compute job request | -| `IntentRehab` | `0x30` | Benevolent intervention (pause execution) | +| `IntentPauseExecution` | `0x30` | Benevolent intervention (pause execution) | | `IntentCustom` | `0xFF` | Application-level sub-protocols | **Sentinel errors**: diff --git a/docs/routing.md b/docs/routing.md index 2bb3405..3881f01 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -84,7 +84,7 @@ Dropped packets are logged at WARN level with the threat score, threshold, inten const ( IntentHandshake byte = 0x01 // Connection establishment / hello IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) + IntentPauseExecution byte = 0x30 IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) ``` diff --git a/docs/ueps.md b/docs/ueps.md index 09c0b71..24ab498 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -156,7 +156,7 @@ Reserved intent values: |----|----------|---------| | `0x01` | `IntentHandshake` | Connection establishment / hello | | `0x20` | `IntentCompute` | Compute job request | -| `0x30` | `IntentRehab` | Benevolent intervention (pause execution) | +| `0x30` | `IntentPauseExecution` | Benevolent intervention (pause execution) | | `0xFF` | `IntentCustom` | Extended / application-level sub-protocols | ## Threat Score diff --git a/node/dispatcher.go b/node/dispatcher.go index faedf4b..53e5cff 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -13,13 +13,12 @@ import ( // threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 -// Well-known intent identifiers. These correspond to the semantic tokens -// carried in the UEPS IntentID header field (RFC-021). +// intentID := IntentPauseExecution const ( - IntentHandshake byte = 0x01 // Connection establishment / hello - IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) - IntentCustom byte = 0xFF // Extended / application-level sub-protocols + IntentHandshake byte = 0x01 + IntentCompute byte = 0x20 + IntentPauseExecution byte = 0x30 + IntentCustom byte = 0xFF ) // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } diff --git a/node/dispatcher_test.go b/node/dispatcher_test.go index 1cbe6e3..d458875 100644 --- a/node/dispatcher_test.go +++ b/node/dispatcher_test.go @@ -136,7 +136,7 @@ func TestDispatcher_UnknownIntentDropped_Bad(t *testing.T) { func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { d := NewDispatcher() - var handshakeCalled, computeCalled, rehabCalled, customCalled bool + var handshakeCalled, computeCalled, pauseExecutionCalled, customCalled bool d.RegisterHandler(IntentHandshake, func(pkt *ueps.ParsedPacket) error { handshakeCalled = true @@ -146,8 +146,8 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { computeCalled = true return nil }) - d.RegisterHandler(IntentRehab, func(pkt *ueps.ParsedPacket) error { - rehabCalled = true + d.RegisterHandler(IntentPauseExecution, func(pkt *ueps.ParsedPacket) error { + pauseExecutionCalled = true return nil }) d.RegisterHandler(IntentCustom, func(pkt *ueps.ParsedPacket) error { @@ -162,7 +162,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { }{ {"handshake routes correctly", IntentHandshake, &handshakeCalled}, {"compute routes correctly", IntentCompute, &computeCalled}, - {"rehab routes correctly", IntentRehab, &rehabCalled}, + {"pause execution routes correctly", IntentPauseExecution, &pauseExecutionCalled}, {"custom routes correctly", IntentCustom, &customCalled}, } @@ -171,7 +171,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { // Reset all flags handshakeCalled = false computeCalled = false - rehabCalled = false + pauseExecutionCalled = false customCalled = false pkt := makePacket(tt.intentID, 0, []byte("payload")) @@ -341,6 +341,6 @@ func TestDispatcher_IntentConstants_Good(t *testing.T) { // Verify the well-known intent IDs match the spec (RFC-021). assert.Equal(t, byte(0x01), IntentHandshake) assert.Equal(t, byte(0x20), IntentCompute) - assert.Equal(t, byte(0x30), IntentRehab) + assert.Equal(t, byte(0x30), IntentPauseExecution) assert.Equal(t, byte(0xFF), IntentCustom) } diff --git a/node/integration_test.go b/node/integration_test.go index bd45880..13d40bc 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -572,7 +572,7 @@ func TestIntegration_DispatcherWithRealUEPSPackets_Good(t *testing.T) { }{ {IntentHandshake, "handshake", "hello"}, {IntentCompute, "compute", `{"job":"123"}`}, - {IntentRehab, "rehab", "pause"}, + {IntentPauseExecution, "pause-execution", "pause"}, {IntentCustom, "custom", "app-specific-data"}, } diff --git a/node/levin/header.go b/node/levin/header.go index 911435a..9da0760 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -11,25 +11,25 @@ import ( core "dappco.re/go/core" ) -// HeaderSize is the exact byte length of a serialised Levin header. +// headerBytes := make([]byte, HeaderSize) const HeaderSize = 33 -// Signature is the magic value that opens every Levin packet. +// header.Signature = Signature const Signature uint64 = 0x0101010101012101 -// MaxPayloadSize is the upper bound we accept for a single payload (100 MB). +// header.PayloadSize <= MaxPayloadSize const MaxPayloadSize uint64 = 100 * 1024 * 1024 -// Return-code constants carried in every Levin response. const ( + // returnCode := ReturnOK ReturnOK int32 = 0 ReturnErrorConnection int32 = -1 ReturnErrorFormat int32 = -7 ReturnErrorSignature int32 = -13 ) -// Command IDs for the CryptoNote P2P layer. const ( + // commandID := CommandHandshake CommandHandshake uint32 = 1001 CommandTimedSync uint32 = 1002 CommandPing uint32 = 1003 @@ -41,8 +41,8 @@ const ( CommandResponseChain uint32 = 2007 ) -// Sentinel errors returned by DecodeHeader. var ( + // err := ErrorBadSignature ErrorBadSignature = core.E("levin", "bad signature", nil) ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) diff --git a/node/message.go b/node/message.go index eb21c41..7fc85e4 100644 --- a/node/message.go +++ b/node/message.go @@ -8,18 +8,14 @@ import ( "github.com/google/uuid" ) -// Protocol version constants. const ( - // ProtocolVersion is the current protocol version. + // version := ProtocolVersion ProtocolVersion = "1.0" - // MinProtocolVersion is the minimum supported version. + // minimumVersion := MinProtocolVersion MinProtocolVersion = "1.0" ) -// SupportedProtocolVersions lists all protocol versions this node supports. -// Used for version negotiation during handshake. -// -// versions := SupportedProtocolVersions +// versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} // payload := RawMessage(`{"pool":"pool.example.com:3333"}`) @@ -50,9 +46,7 @@ func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } -// MessageType defines the type of P2P message. -// -// messageType := MessagePing +// messageType := MessagePing type MessageType string const ( @@ -241,12 +235,12 @@ type ErrorPayload struct { Details string `json:"details,omitempty"` } -// Common error codes. const ( - ErrorCodeUnknown = 1000 - ErrorCodeInvalidMessage = 1001 - ErrorCodeUnauthorized = 1002 - ErrorCodeNotFound = 1003 + ErrorCodeUnknown = 1000 + ErrorCodeInvalidMessage = 1001 + ErrorCodeUnauthorized = 1002 + ErrorCodeNotFound = 1003 + // code := ErrorCodeOperationFailed ErrorCodeOperationFailed = 1004 ErrorCodeTimeout = 1005 ) diff --git a/node/protocol.go b/node/protocol.go index 23d5152..0565e76 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -58,16 +58,12 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, // handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} -// ValidateResponse is a convenience function using the default handler. -// -// err := ValidateResponse(message, MessageStats) +// err := ValidateResponse(message, MessageStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } -// ParseResponse is a convenience function using the default handler. -// -// err := ParseResponse(message, MessageStats, &stats) +// err := ParseResponse(message, MessageStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } diff --git a/node/transport.go b/node/transport.go index e2c74e9..f537787 100644 --- a/node/transport.go +++ b/node/transport.go @@ -27,10 +27,10 @@ var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). const messageLogSampleInterval = 100 -// DefaultMaxMessageSize is the default maximum message size (1MB) +// limit := DefaultMaxMessageSize const DefaultMaxMessageSize int64 = 1 << 20 // 1MB -// agentUserAgentPrefix identifies this tool in request headers. +// prefix := agentUserAgentPrefix const agentUserAgentPrefix = "agent-go-p2p" const ( @@ -41,14 +41,14 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // ":9091" default - WebSocketPath string // "/ws" - WebSocket endpoint path - TLSCertPath string // Optional TLS for wss:// - TLSKeyPath string - MaxConnections int // Maximum concurrent connections - MaxMessageSize int64 // Maximum message size in bytes (0 = 1MB default) - PingInterval time.Duration // WebSocket keepalive interval - PongTimeout time.Duration // Timeout waiting for pong + ListenAddr string // config.ListenAddr = ":9091" + WebSocketPath string // config.WebSocketPath = "/ws" + TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" + TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" + MaxConnections int // config.MaxConnections = 100 + MaxMessageSize int64 // config.MaxMessageSize = 1 << 20 + PingInterval time.Duration // config.PingInterval = 30 * time.Second + PongTimeout time.Duration // config.PongTimeout = 10 * time.Second } // transportConfig := DefaultTransportConfig() diff --git a/node/worker.go b/node/worker.go index 3de7277..073cdfc 100644 --- a/node/worker.go +++ b/node/worker.go @@ -39,7 +39,7 @@ type Worker struct { minerManager MinerManager profileManager ProfileManager startedAt time.Time - DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) + DeploymentDirectory string // worker.DeploymentDirectory = "/srv/p2p/deployments" } // worker := NewWorker(nodeManager, transport) @@ -395,9 +395,7 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// RegisterOnTransport installs the worker message handler on the transport. -// -// worker.RegisterOnTransport() +// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -- 2.45.3 From 194fe707de51f59191c7888a1fd3e6e1390cd37d Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:19:03 +0000 Subject: [PATCH 57/60] refactor(node): align AX comments and peer copy semantics Co-Authored-By: Virgil --- docs/architecture.md | 10 ++++----- docs/discovery.md | 8 +++---- docs/history.md | 6 ++--- docs/identity.md | 14 ++++++------ docs/transport.md | 12 +++++----- node/bundle.go | 8 ++----- node/identity.go | 5 ++--- node/peer.go | 33 +++++++++++++++++++++++----- node/peer_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ node/transport.go | 33 +++++++++++++++++++++++----- node/transport_test.go | 11 ++++++++++ 11 files changed, 146 insertions(+), 44 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index bb4b90c..8771504 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. -**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMu` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. +**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMutex` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. **Buffer pool**: `MarshalJSON` uses a `sync.Pool` of `bytes.Buffer` (initial capacity 1 KB, maximum pooled size 64 KB) to reduce allocation pressure in the message serialisation hot path. HTML escaping is disabled to match `json.Marshal` semantics. @@ -70,15 +70,15 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Peer fields persisted**: - `ID`, `Name`, `PublicKey`, `Address`, `Role`, `AddedAt`, `LastSeen` -- `PingMS`, `Hops`, `GeoKM`, `Score` (float64, 0–100) +- `PingMilliseconds`, `Hops`, `GeographicKilometres`, `Score` (float64, 0–100) **KD-tree dimensions** (lower is better in all axes): | Dimension | Weight | Rationale | |-----------|--------|-----------| -| `PingMS` | 1.0 | Latency dominates interactive performance | +| `PingMilliseconds` | 1.0 | Latency dominates interactive performance | | `Hops` | 0.7 | Network hop count (routing cost) | -| `GeoKM` | 0.2 | Geographic distance (minor factor) | +| `GeographicKilometres` | 0.2 | Geographic distance (minor factor) | | `100 - Score` | 1.2 | Reliability (inverted so lower = better peer) | `SelectOptimalPeer()` queries the tree for the point nearest to the origin (ideal: zero latency, zero hops, zero distance, maximum score). `SelectNearestPeers(n)` returns the n best. @@ -236,7 +236,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. |----------|------------| | `Transport.conns` | `sync.RWMutex` | | `Transport.handler` | `sync.RWMutex` | -| `PeerConnection` writes | `sync.Mutex` (`writeMu`) | +| `PeerConnection` writes | `sync.Mutex` (`writeMutex`) | | `PeerConnection` close | `sync.Once` (`closeOnce`) | | `PeerRegistry.peers` + KD-tree | `sync.RWMutex` | | `PeerRegistry.allowedPublicKeys` | separate `sync.RWMutex` | diff --git a/docs/discovery.md b/docs/discovery.md index 3423165..452bb94 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -20,9 +20,9 @@ type Peer struct { LastSeen time.Time `json:"lastSeen"` // Poindexter metrics (updated dynamically) - PingMS float64 `json:"pingMs"` // Latency in milliseconds + PingMilliseconds float64 `json:"pingMs"` // Latency in milliseconds Hops int `json:"hops"` // Network hop count - GeoKM float64 `json:"geoKm"` // Geographic distance in kilometres + GeographicKilometres float64 `json:"geoKm"` // Geographic distance in kilometres Score float64 `json:"score"` // Reliability score 0--100 Connected bool `json:"-"` // Not persisted @@ -83,9 +83,9 @@ The registry maintains a 4-dimensional KD-tree for optimal peer selection. Each | Dimension | Source | Weight | Direction | |-----------|--------|--------|-----------| -| Latency | `PingMS` | 1.0 | Lower is better | +| Latency | `PingMilliseconds` | 1.0 | Lower is better | | Hops | `Hops` | 0.7 | Lower is better | -| Geographic distance | `GeoKM` | 0.2 | Lower is better | +| Geographic distance | `GeographicKilometres` | 0.2 | Lower is better | | Reliability | `100 - Score` | 1.2 | Inverted so lower is better | The score dimension is inverted so that the "ideal peer" target point `[0, 0, 0, 0]` represents zero latency, zero hops, zero distance, and maximum reliability (score 100). diff --git a/docs/history.md b/docs/history.md index 5c42f56..5c42f07 100644 --- a/docs/history.md +++ b/docs/history.md @@ -31,7 +31,7 @@ Tests covered: - MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close -- Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) +- Concurrent sends: no data races under `go test -race` (`writeMutex` protects all writes) ### Phase 3 — Controller Tests @@ -104,9 +104,9 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMu`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. -Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMu`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. +Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. ## Wiki Corrections (19 February 2026) diff --git a/docs/identity.md b/docs/identity.md index b102683..05c1a58 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -39,13 +39,13 @@ Paths follow XDG base directories via `github.com/adrg/xdg`. The private key is ### Creating an Identity ```go -nm, err := node.NewNodeManager() +nodeManager, err := node.NewNodeManager() if err != nil { log.Fatal(err) } // Generate a new identity (persists key and config to disk) -err = nm.GenerateIdentity("eu-controller-01", node.RoleController) +err = nodeManager.GenerateIdentity("eu-controller-01", node.RoleController) ``` Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce the X25519 keypair. @@ -53,7 +53,7 @@ Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce ### Custom Paths (Testing) ```go -nm, err := node.NewNodeManagerFromPaths( +nodeManager, err := node.NewNodeManagerFromPaths( "/tmp/test/private.key", "/tmp/test/node.json", ) @@ -62,8 +62,8 @@ nm, err := node.NewNodeManagerFromPaths( ### Checking and Retrieving Identity ```go -if nm.HasIdentity() { - identity := nm.GetIdentity() // Returns a copy +if nodeManager.HasIdentity() { + identity := nodeManager.GetIdentity() // Returns a copy fmt.Println(identity.ID, identity.Name) } ``` @@ -73,7 +73,7 @@ if nm.HasIdentity() { ### Deriving Shared Secrets ```go -sharedSecret, err := nm.DeriveSharedSecret(peerPublicKeyBase64) +sharedSecret, err := nodeManager.DeriveSharedSecret(peerPublicKeyBase64) ``` This performs X25519 ECDH with the peer's public key and hashes the result with SHA-256, producing a 32-byte symmetric key. The same shared secret is derived independently by both sides (no secret is transmitted). @@ -81,7 +81,7 @@ This performs X25519 ECDH with the peer's public key and hashes the result with ### Deleting an Identity ```go -err := nm.Delete() // Removes key and config from disk, clears in-memory state +err := nodeManager.Delete() // Removes key and config from disk, clears in-memory state ``` ## Challenge-Response Authentication diff --git a/docs/transport.md b/docs/transport.md index 5aad542..07a25e6 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -25,7 +25,7 @@ type TransportConfig struct { Sensible defaults via `DefaultTransportConfig()`: ```go -cfg := node.DefaultTransportConfig() +transportConfig := node.DefaultTransportConfig() // ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -33,10 +33,10 @@ cfg := node.DefaultTransportConfig() ## Creating and Starting ```go -transport := node.NewTransport(nodeManager, peerRegistry, cfg) +transport := node.NewTransport(nodeManager, peerRegistry, transportConfig) // Set message handler before Start() to avoid races -transport.OnMessage(func(conn *node.PeerConnection, msg *node.Message) { +transport.OnMessage(func(peerConnection *node.PeerConnection, msg *node.Message) { // Handle incoming messages }) @@ -96,15 +96,15 @@ type PeerConnection struct { ### Sending Messages ```go -err := peerConn.Send(msg) +err := peerConnection.Send(msg) ``` -`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMu` mutex serialises concurrent writes. +`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMutex` serialises concurrent writes. ### Graceful Close ```go -err := peerConn.GracefulClose("shutting down", node.DisconnectShutdown) +err := peerConnection.GracefulClose("shutting down", node.DisconnectShutdown) ``` Sends a `disconnect` message (best-effort) before closing the connection. Uses `sync.Once` to ensure the connection is only closed once. diff --git a/node/bundle.go b/node/bundle.go index f1c7342..30e409b 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -347,9 +347,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return firstExecutable, nil } -// StreamBundle writes a bundle to a writer (for large transfers). -// -// err := StreamBundle(bundle, writer) +// err := StreamBundle(bundle, writer) func StreamBundle(bundle *Bundle, w io.Writer) error { result := core.JSONMarshal(bundle) if !result.OK { @@ -359,9 +357,7 @@ func StreamBundle(bundle *Bundle, w io.Writer) error { return err } -// ReadBundle reads a bundle from a reader. -// -// bundle, err := ReadBundle(reader) +// bundle, err := ReadBundle(reader) func ReadBundle(r io.Reader) (*Bundle, error) { var buf bytes.Buffer if _, err := io.Copy(&buf, r); err != nil { diff --git a/node/identity.go b/node/identity.go index ae5d1f1..7bb3a1d 100644 --- a/node/identity.go +++ b/node/identity.go @@ -87,10 +87,9 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// Missing files are treated as a fresh install; malformed or partial identity -// state is returned as an error so callers can handle it explicitly. -// // nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// Missing files are treated as a fresh install; malformed or partial identity +// state returns an error so callers can handle it explicitly. func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, diff --git a/node/peer.go b/node/peer.go index 42f8801..27a80d7 100644 --- a/node/peer.go +++ b/node/peer.go @@ -131,10 +131,9 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// Missing files are treated as an empty registry; malformed registry files are -// returned as errors so callers can repair the persisted state. -// // peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// Missing files are treated as an empty registry; malformed registry files +// return an error so callers can repair the persisted state. func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -248,6 +247,13 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { // // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.AddPeer", "peer is nil", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if peer.ID == "" { @@ -271,7 +277,7 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { peer.AddedAt = time.Now() } if peer.Score == 0 { - peer.Score = 50 // Default neutral score + peer.Score = ScoreDefault } r.peers[peer.ID] = peer @@ -286,6 +292,17 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { // // err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.UpdatePeer", "peer is nil", nil) + } + + if peer.ID == "" { + return core.E("PeerRegistry.UpdatePeer", "peer ID is required", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if _, exists := r.peers[peer.ID]; !exists { @@ -502,7 +519,13 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return 0 }) - return peers + peerCopies := make([]*Peer, 0, len(peers)) + for _, peer := range peers { + peerCopy := *peer + peerCopies = append(peerCopies, &peerCopy) + } + + return peerCopies } // for peer := range registry.PeersByScore() { diff --git a/node/peer_test.go b/node/peer_test.go index b10979c..45d6f0f 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -65,6 +65,15 @@ func TestPeer_Registry_AddPeer_Good(t *testing.T) { t.Errorf("expected 1 peer, got %d", pr.Count()) } + peer.Name = "Mutated after add" + stored := pr.GetPeer("test-peer-1") + if stored == nil { + t.Fatal("expected peer to exist after add") + } + if stored.Name != "Test Peer" { + t.Errorf("expected stored peer to remain unchanged, got %q", stored.Name) + } + // Try to add duplicate err = pr.AddPeer(peer) if err == nil { @@ -634,6 +643,34 @@ func TestPeer_Registry_PeersSortedByScore_Good(t *testing.T) { if sorted[2].ID != "low-score" { t.Errorf("third peer should be low-score, got %s", sorted[2].ID) } + + sorted[0].Name = "Mutated" + restored := pr.GetPeer("high-score") + if restored == nil { + t.Fatal("expected high-score peer to still exist") + } + if restored.Name != "High" { + t.Errorf("expected registry peer to remain unchanged, got %q", restored.Name) + } +} + +func TestPeer_Registry_NilPeerInputs_Bad(t *testing.T) { + pr, cleanup := setupTestPeerRegistry(t) + defer cleanup() + + t.Run("AddPeer", func(t *testing.T) { + err := pr.AddPeer(nil) + if err == nil { + t.Fatal("expected error when adding nil peer") + } + }) + + t.Run("UpdatePeer", func(t *testing.T) { + err := pr.UpdatePeer(nil) + if err == nil { + t.Fatal("expected error when updating nil peer") + } + }) } // --- Additional coverage tests for peer.go --- @@ -736,6 +773,19 @@ func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { if updated.Score != 80 { t.Errorf("expected score 80, got %f", updated.Score) } + + peer.Name = "Mutated after update" + peer.Score = 12 + stored := pr.GetPeer("update-test") + if stored == nil { + t.Fatal("expected peer to exist after update mutation") + } + if stored.Name != "Updated" { + t.Errorf("expected stored peer name to remain Updated, got %q", stored.Name) + } + if stored.Score != 80 { + t.Errorf("expected stored peer score to remain 80, got %f", stored.Score) + } } func TestPeer_Registry_UpdateMetrics_NotFound_Bad(t *testing.T) { diff --git a/node/transport.go b/node/transport.go index f537787..9c93165 100644 --- a/node/transport.go +++ b/node/transport.go @@ -107,22 +107,45 @@ func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator return d } -// IsDuplicate checks whether a message ID is still within the retention window. +// duplicate := deduplicator.IsDuplicate(message.ID) func (d *MessageDeduplicator) IsDuplicate(msgID string) bool { d.mutex.RLock() - _, exists := d.recentMessageTimes[msgID] + seenAt, exists := d.recentMessageTimes[msgID] + retentionWindow := d.timeToLive d.mutex.RUnlock() - return exists + + if !exists { + return false + } + + if retentionWindow > 0 && time.Since(seenAt) <= retentionWindow { + return true + } + + d.mutex.Lock() + defer d.mutex.Unlock() + + seenAt, exists = d.recentMessageTimes[msgID] + if !exists { + return false + } + + if retentionWindow <= 0 || time.Since(seenAt) > retentionWindow { + delete(d.recentMessageTimes, msgID) + return false + } + + return true } -// Mark records a message ID as recently seen. +// deduplicator.Mark(message.ID) func (d *MessageDeduplicator) Mark(msgID string) { d.mutex.Lock() d.recentMessageTimes[msgID] = time.Now() d.mutex.Unlock() } -// Cleanup removes expired entries from the deduplicator. +// deduplicator.Cleanup() func (d *MessageDeduplicator) Cleanup() { d.mutex.Lock() defer d.mutex.Unlock() diff --git a/node/transport_test.go b/node/transport_test.go index 24969b1..16acd46 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -153,6 +153,17 @@ func TestTransport_MessageDeduplicator_Good(t *testing.T) { } }) + t.Run("ExpiredEntriesDoNotLinger", func(t *testing.T) { + d := NewMessageDeduplicator(50 * time.Millisecond) + d.Mark("msg-1") + + time.Sleep(75 * time.Millisecond) + + if d.IsDuplicate("msg-1") { + t.Error("should not be duplicate after TTL even before cleanup runs") + } + }) + t.Run("ConcurrentAccess", func(t *testing.T) { d := NewMessageDeduplicator(5 * time.Minute) var wg sync.WaitGroup -- 2.45.3 From 9c5f3d0bd3184700251c9ccbf3217c74013a1627 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:25:37 +0000 Subject: [PATCH 58/60] refactor(node): trim redundant AX comments Co-Authored-By: Virgil --- node/buffer_pool.go | 2 -- node/bundle.go | 20 -------------------- node/controller.go | 12 ------------ node/errors.go | 2 -- node/identity.go | 9 --------- node/levin/connection.go | 2 -- node/peer.go | 6 ------ node/worker.go | 23 ----------------------- 8 files changed, 76 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index 91f160d..b883e09 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -15,14 +15,12 @@ var bufferPool = sync.Pool{ }, } -// getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { buffer := bufferPool.Get().(*bytes.Buffer) buffer.Reset() return buffer } -// putBuffer returns a buffer to the pool. func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) if buffer.Cap() <= 65536 { diff --git a/node/bundle.go b/node/bundle.go index 30e409b..034fd95 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -46,20 +46,17 @@ type BundleManifest struct { // bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { - // Create a TIM with just the profile config timBundle, err := tim.New() if err != nil { return nil, core.E("CreateProfileBundle", "failed to create TIM", err) } timBundle.Config = profileJSON - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateProfileBundle", "failed to encrypt bundle", err) } - // Calculate checksum checksum := calculateChecksum(stimData) return &Bundle{ @@ -84,14 +81,12 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e // bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { - // Read miner binary minerContent, err := filesystemRead(minerPath) if err != nil { return nil, core.E("CreateMinerBundle", "failed to read miner binary", err) } minerData := []byte(minerContent) - // Create a tarball with the miner binary tarData, err := createTarball(map[string][]byte{ core.PathBase(minerPath): minerData, }) @@ -99,24 +94,20 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo return nil, core.E("CreateMinerBundle", "failed to create tarball", err) } - // Create DataNode from tarball dataNode, err := datanode.FromTar(tarData) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create datanode", err) } - // Create TIM from DataNode timBundle, err := tim.FromDataNode(dataNode) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create TIM", err) } - // Set profile as config if provided if profileJSON != nil { timBundle.Config = profileJSON } - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateMinerBundle", "failed to encrypt bundle", err) @@ -134,17 +125,14 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo // profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { - // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { return nil, core.E("ExtractProfileBundle", "checksum mismatch - bundle may be corrupted", nil) } - // If it's unencrypted JSON, just return it if isJSON(bundle.Data) { return bundle.Data, nil } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return nil, core.E("ExtractProfileBundle", "failed to decrypt bundle", err) @@ -155,24 +143,20 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { - // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { return "", nil, core.E("ExtractMinerBundle", "checksum mismatch - bundle may be corrupted", nil) } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to decrypt bundle", err) } - // Convert rootfs to tarball and extract tarData, err := timBundle.RootFS.ToTar() if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) } - // Extract tarball to destination minerPath, err := extractTarball(tarData, destDir) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to extract tarball", err) @@ -186,13 +170,11 @@ func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } -// calculateChecksum computes SHA-256 checksum of data. func calculateChecksum(data []byte) string { hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } -// isJSON checks if data starts with JSON characters. func isJSON(data []byte) bool { if len(data) == 0 { return false @@ -201,7 +183,6 @@ func isJSON(data []byte) bool { return data[0] == '{' || data[0] == '[' } -// createTarball creates a tar archive from a map of filename -> content. func createTarball(files map[string][]byte) ([]byte, error) { var buf bytes.Buffer tarWriter := tar.NewWriter(&buf) @@ -248,7 +229,6 @@ func createTarball(files map[string][]byte) ([]byte, error) { return buf.Bytes(), nil } -// extractTarball extracts a tar archive to a directory, returns first executable found. func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir := destDir diff --git a/node/controller.go b/node/controller.go index 17bbc22..8441352 100644 --- a/node/controller.go +++ b/node/controller.go @@ -36,7 +36,6 @@ func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transpo return c } -// handleResponse processes incoming replies and routes them to the waiting request. func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { if message.ReplyTo == "" { return // Not a response, let worker handle it @@ -58,12 +57,6 @@ func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { } } -// sendRequest registers a temporary response channel, sends message, and waits -// for the matching reply or timeout. -// -// The response channel is intentionally never closed. Removing it from the -// pending map is enough to stop future routing, and it avoids a late-response -// close/send race after the caller has already timed out. func (c *Controller) sendRequest(peerID string, message *Message, timeout time.Duration) (*Message, error) { resolvedPeerID := peerID @@ -77,13 +70,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D if err != nil { return nil, core.E("Controller.sendRequest", "failed to connect to peer", err) } - // Use the real peer ID after handshake (it may have changed) resolvedPeerID = conn.Peer.ID - // Update the message destination message.To = resolvedPeerID } - // Create response channel responseChannel := make(chan *Message, 1) c.mutex.Lock() @@ -98,12 +88,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D c.mutex.Unlock() }() - // Send the message if err := c.transport.Send(resolvedPeerID, message); err != nil { return nil, core.E("Controller.sendRequest", "failed to send message", err) } - // Wait for response ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() diff --git a/node/errors.go b/node/errors.go index 302a34a..26ac3d6 100644 --- a/node/errors.go +++ b/node/errors.go @@ -3,9 +3,7 @@ package node import core "dappco.re/go/core" var ( - // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) diff --git a/node/identity.go b/node/identity.go index 7bb3a1d..9679305 100644 --- a/node/identity.go +++ b/node/identity.go @@ -199,15 +199,12 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error return hash[:], nil } -// savePrivateKey saves the private key to disk with restricted permissions. func (n *NodeManager) savePrivateKey() error { - // Ensure directory exists dir := core.PathDir(n.keyPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } - // Write private key if err := filesystemWrite(n.keyPath, string(n.privateKey)); err != nil { return core.E("NodeManager.savePrivateKey", "failed to write private key", err) } @@ -215,9 +212,7 @@ func (n *NodeManager) savePrivateKey() error { return nil } -// saveIdentity saves the public identity to the config file. func (n *NodeManager) saveIdentity() error { - // Ensure directory exists dir := core.PathDir(n.configPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.saveIdentity", "failed to create config directory", err) @@ -236,9 +231,7 @@ func (n *NodeManager) saveIdentity() error { return nil } -// loadIdentity loads the node identity from disk. func (n *NodeManager) loadIdentity() error { - // Load identity config content, err := filesystemRead(n.configPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read identity", err) @@ -250,14 +243,12 @@ func (n *NodeManager) loadIdentity() error { return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", result.Value.(error)) } - // Load private key keyContent, err := filesystemRead(n.keyPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read private key", err) } privateKey := []byte(keyContent) - // Reconstruct keypair from private key keyPair, err := stmf.LoadKeyPair(privateKey) if err != nil { return core.E("NodeManager.loadIdentity", "failed to load keypair", err) diff --git a/node/levin/connection.go b/node/levin/connection.go index 76bd643..a479b04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -79,7 +79,6 @@ func (connection *Connection) WriteResponse(commandID uint32, payload []byte, re return connection.writeFrame(&header, payload) } -// writeFrame serialises header + payload and writes them atomically. func (connection *Connection) writeFrame(header *Header, payload []byte) error { headerBytes := EncodeHeader(header) @@ -109,7 +108,6 @@ func (connection *Connection) ReadPacket() (Header, []byte, error) { return Header{}, nil, err } - // Read header. var headerBytes [HeaderSize]byte if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err diff --git a/node/peer.go b/node/peer.go index 27a80d7..55e9fa6 100644 --- a/node/peer.go +++ b/node/peer.go @@ -64,7 +64,6 @@ const ( // peerNamePattern validates peer names: alphanumeric, hyphens, underscores, and spaces. var peerNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-_ ]{0,62}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`) -// safeKeyPrefix returns a truncated key for logging, handling short keys safely func safeKeyPrefix(key string) string { if len(key) >= 16 { return key[:16] + "..." @@ -75,9 +74,6 @@ func safeKeyPrefix(key string) string { return key } -// validatePeerName checks if a peer name is valid. -// Peer names must be 1-64 characters, start and end with alphanumeric, -// and contain only alphanumeric, hyphens, underscores, and spaces. func validatePeerName(name string) error { if name == "" { return nil // Empty names are allowed (optional field) @@ -347,7 +343,6 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return nil } - // Return a copy peerCopy := *peer return &peerCopy } @@ -508,7 +503,6 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { peers := slices.Collect(maps.Values(r.peers)) - // Sort by score descending slices.SortFunc(peers, func(a, b *Peer) int { if b.Score > a.Score { return 1 diff --git a/node/worker.go b/node/worker.go index 073cdfc..03875f2 100644 --- a/node/worker.go +++ b/node/worker.go @@ -111,7 +111,6 @@ func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) } } -// handlePing responds to ping requests. func (w *Worker) handlePing(message *Message) (*Message, error) { var ping PingPayload if err := message.ParsePayload(&ping); err != nil { @@ -126,7 +125,6 @@ func (w *Worker) handlePing(message *Message) (*Message, error) { return message.Reply(MessagePong, pong) } -// handleStats responds with current miner statistics. func (w *Worker) handleStats(message *Message) (*Message, error) { identity := w.nodeManager.GetIdentity() if identity == nil { @@ -148,8 +146,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { continue } - // Convert to MinerStatsItem - this is a simplified conversion - // The actual implementation would need to match the mining package's stats structure item := convertMinerStats(miner, minerStats) stats.Miners = append(stats.Miners, item) } @@ -158,7 +154,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { return message.Reply(MessageStats, stats) } -// convertMinerStats converts a running miner's raw stats map to the wire protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ Name: miner.GetName(), @@ -189,7 +184,6 @@ func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { return item } -// handleStartMiner starts a miner with the given profile. func (w *Worker) handleStartMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -200,12 +194,10 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "invalid start miner payload", err) } - // Validate miner type is provided if payload.MinerType == "" { return nil, core.E("Worker.handleStartMiner", "miner type is required", nil) } - // Get the config from the profile or use the override var config any if payload.Config != nil { config = payload.Config @@ -219,7 +211,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "no config provided and no profile manager configured", nil) } - // Start the miner miner, err := w.minerManager.StartMiner(payload.MinerType, config) if err != nil { ack := MinerAckPayload{ @@ -236,7 +227,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleStopMiner stops a running miner. func (w *Worker) handleStopMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -259,7 +249,6 @@ func (w *Worker) handleStopMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleLogs returns console logs from a miner. func (w *Worker) handleLogs(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -270,7 +259,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return nil, core.E("Worker.handleLogs", "invalid logs payload", err) } - // Validate and limit the Lines parameter to prevent resource exhaustion const maxLogLines = 10000 if payload.Lines <= 0 || payload.Lines > maxLogLines { payload.Lines = maxLogLines @@ -292,7 +280,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return message.Reply(MessageLogs, logs) } -// handleDeploy handles deployment of profiles or miner bundles. func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) (*Message, error) { var payload DeployPayload if err := message.ParsePayload(&payload); err != nil { @@ -319,13 +306,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return nil, core.E("Worker.handleDeploy", "profile manager not configured", nil) } - // Decrypt and extract profile data profileData, err := ExtractProfileBundle(bundle, password) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract profile bundle", err) } - // Unmarshal into interface{} to pass to ProfileManager var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", result.Value.(error)) @@ -347,8 +332,6 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return message.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: - // Determine the installation directory under the configured deployment - // root for lethean-desktop/miners/. minersDir := core.JoinPath(w.deploymentDirectory(), "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) @@ -358,13 +341,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) "type": payload.BundleType, }) - // Extract miner bundle minerPath, profileData, err := ExtractMinerBundle(bundle, password, installDir) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract miner bundle", err) } - // If the bundle contained a profile config, save it if len(profileData) > 0 && w.profileManager != nil { var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { @@ -376,13 +357,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } - // Success response ack := DeployAckPayload{ Success: true, Name: payload.Name, } - // Log the installation logging.Info("miner bundle installed successfully", logging.Fields{ "name": payload.Name, "miner_path": minerPath, @@ -395,12 +374,10 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -// deploymentDirectory resolves the active deployment directory. func (w *Worker) deploymentDirectory() string { if w.DeploymentDirectory != "" { return w.DeploymentDirectory -- 2.45.3 From fb9c9188573dbd4fd685a422f46744a3b475a7f7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:32:55 +0000 Subject: [PATCH 59/60] refactor(node): align transport naming with AX Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/history.md | 2 +- docs/transport.md | 8 +-- node/peer.go | 6 -- node/transport.go | 133 +++++++++++++++++++++++++---------------- node/transport_test.go | 4 +- specs/node.md | 6 +- 7 files changed, 93 insertions(+), 68 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8771504..dc204a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| -| `ListenAddr` | `:9091` | HTTP bind address | +| `ListenAddress` | `:9091` | HTTP bind address | | `WebSocketPath` | `/ws` | WebSocket endpoint | | `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | diff --git a/docs/history.md b/docs/history.md index 5c42f07..986727f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -104,7 +104,7 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.WebSocketConnection.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. diff --git a/docs/transport.md b/docs/transport.md index 07a25e6..56b55f9 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -11,7 +11,7 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { - ListenAddr string // ":9091" default + ListenAddress string // ":9091" default WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go transportConfig := node.DefaultTransportConfig() -// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 +// ListenAddress: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -86,8 +86,8 @@ Each active connection is wrapped in a `PeerConnection`: ```go type PeerConnection struct { - Peer *Peer // Remote peer identity - Conn *websocket.Conn // Underlying WebSocket + Peer *Peer // Remote peer identity + WebSocketConnection *websocket.Conn // Underlying WebSocket SharedSecret []byte // From X25519 ECDH LastActivity time.Time } diff --git a/node/peer.go b/node/peer.go index 55e9fa6..2b7606b 100644 --- a/node/peer.go +++ b/node/peer.go @@ -42,7 +42,6 @@ type Peer struct { Connected bool `json:"-"` } -// peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second // mode := PeerAuthAllowlist @@ -239,8 +238,6 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. -// // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { if peer == nil { @@ -623,8 +620,6 @@ func (r *PeerRegistry) Count() int { return len(r.peers) } -// rebuildKDTree rebuilds the KD-tree from current peers. -// Must be called with lock held. func (r *PeerRegistry) rebuildKDTree() { if len(r.peers) == 0 { r.kdTree = nil @@ -746,7 +741,6 @@ func (r *PeerRegistry) Close() error { return nil } -// load reads peers from disk. func (r *PeerRegistry) load() error { content, err := filesystemRead(r.path) if err != nil { diff --git a/node/transport.go b/node/transport.go index 9c93165..6004aed 100644 --- a/node/transport.go +++ b/node/transport.go @@ -21,7 +21,6 @@ import ( "github.com/gorilla/websocket" ) -// messageLogSampleCounter tracks message counts for sampled debug logs. var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). @@ -41,7 +40,8 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // config.ListenAddr = ":9091" + ListenAddress string // config.ListenAddress = ":9091" + ListenAddr string WebSocketPath string // config.WebSocketPath = "/ws" TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" @@ -54,6 +54,7 @@ type TransportConfig struct { // transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ + ListenAddress: defaultTransportListenAddress, ListenAddr: defaultTransportListenAddress, WebSocketPath: defaultTransportWebSocketPath, MaxConnections: defaultTransportMaximumConnections, @@ -63,16 +64,22 @@ func DefaultTransportConfig() TransportConfig { } } -// listenAddress returns the effective listen address, falling back to the -// default when the config leaves it empty. func (c TransportConfig) listenAddress() string { + if c.ListenAddress != "" && c.ListenAddress != defaultTransportListenAddress { + return c.ListenAddress + } + if c.ListenAddr != "" && c.ListenAddr != defaultTransportListenAddress { + return c.ListenAddr + } + if c.ListenAddress != "" { + return c.ListenAddress + } if c.ListenAddr != "" { return c.ListenAddr } return defaultTransportListenAddress } -// webSocketPath returns the effective WebSocket endpoint path. func (c TransportConfig) webSocketPath() string { if c.WebSocketPath != "" { return c.WebSocketPath @@ -80,7 +87,6 @@ func (c TransportConfig) webSocketPath() string { return defaultTransportWebSocketPath } -// maximumConnections returns the effective concurrent connection limit. func (c TransportConfig) maximumConnections() int { if c.MaxConnections > 0 { return c.MaxConnections @@ -217,15 +223,16 @@ func (r *PeerRateLimiter) Allow() bool { // peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { - Peer *Peer - Conn *websocket.Conn - SharedSecret []byte // Derived via X25519 ECDH, used for SMSG - LastActivity time.Time - UserAgent string // Request identity advertised by the peer - writeMutex sync.Mutex // Serialize WebSocket writes - transport *Transport - closeOnce sync.Once // Ensure Close() is only called once - rateLimiter *PeerRateLimiter // Per-peer message rate limiting + Peer *Peer + WebSocketConnection *websocket.Conn + Conn *websocket.Conn + SharedSecret []byte // Derived via X25519 ECDH, used for SMSG + LastActivity time.Time + UserAgent string // Request identity advertised by the peer + writeMutex sync.Mutex // Serialize WebSocket writes + transport *Transport + closeOnce sync.Once // Ensure Close() is only called once + rateLimiter *PeerRateLimiter // Per-peer message rate limiting } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) @@ -261,7 +268,13 @@ func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config T } } -// agentHeaderToken converts free-form identity data into a stable header token. +func (pc *PeerConnection) webSocketConnection() *websocket.Conn { + if pc.WebSocketConnection != nil { + return pc.WebSocketConnection + } + return pc.Conn +} + func agentHeaderToken(value string) string { value = strings.TrimSpace(value) if value == "" { @@ -295,7 +308,6 @@ func agentHeaderToken(value string) string { return token } -// agentUserAgent returns a transparent identity string for request headers. func (t *Transport) agentUserAgent() string { identity := t.nodeManager.GetIdentity() if identity == nil { @@ -437,12 +449,13 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } peerConnection := &PeerConnection{ - Peer: peer, - Conn: conn, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } // Perform handshake with challenge-response authentication. @@ -531,7 +544,6 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { return t.connections[peerID] } -// handleWebSocketUpgrade handles incoming WebSocket connections. func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -668,13 +680,14 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques } pc := &PeerConnection{ - Peer: peer, - Conn: conn, - SharedSecret: sharedSecret, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + SharedSecret: sharedSecret, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } identity := t.nodeManager.GetIdentity() @@ -738,16 +751,20 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques go t.keepalive(pc) } -// performHandshake initiates handshake with a peer. func (t *Transport) performHandshake(pc *PeerConnection) error { // Set handshake timeout handshakeTimeout := 10 * time.Second - pc.Conn.SetWriteDeadline(time.Now().Add(handshakeTimeout)) - pc.Conn.SetReadDeadline(time.Now().Add(handshakeTimeout)) + connection := pc.webSocketConnection() + if connection == nil { + return core.E("Transport.performHandshake", "websocket connection is nil", nil) + } + + connection.SetWriteDeadline(time.Now().Add(handshakeTimeout)) + connection.SetReadDeadline(time.Now().Add(handshakeTimeout)) defer func() { // Reset deadlines after handshake - pc.Conn.SetWriteDeadline(time.Time{}) - pc.Conn.SetReadDeadline(time.Time{}) + connection.SetWriteDeadline(time.Time{}) + connection.SetReadDeadline(time.Time{}) }() identity := t.nodeManager.GetIdentity() @@ -778,12 +795,12 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return core.E("Transport.performHandshake", "marshal handshake message", err) } - if err := pc.Conn.WriteMessage(websocket.TextMessage, data); err != nil { + if err := connection.WriteMessage(websocket.TextMessage, data); err != nil { return core.E("Transport.performHandshake", "send handshake", err) } // Wait for ack - _, ackData, err := pc.Conn.ReadMessage() + _, ackData, err := connection.ReadMessage() if err != nil { return core.E("Transport.performHandshake", "read handshake ack", err) } @@ -844,7 +861,6 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return nil } -// readLoop reads messages from a peer connection. func (t *Transport) readLoop(pc *PeerConnection) { defer t.waitGroup.Done() defer t.removeConnection(pc) @@ -854,7 +870,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { if maxSize <= 0 { maxSize = DefaultMaxMessageSize } - pc.Conn.SetReadLimit(maxSize) + connection := pc.webSocketConnection() + if connection == nil { + return + } + + connection.SetReadLimit(maxSize) for { select { @@ -865,12 +886,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { // Set read deadline to prevent blocking forever on unresponsive connections readDeadline := t.config.PingInterval + t.config.PongTimeout - if err := pc.Conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { + if err := connection.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { logging.Error("SetReadDeadline error", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return } - _, data, err := pc.Conn.ReadMessage() + _, data, err := connection.ReadMessage() if err != nil { logging.Debug("read error from peer", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return @@ -913,7 +934,6 @@ func (t *Transport) readLoop(pc *PeerConnection) { } } -// keepalive sends periodic pings. func (t *Transport) keepalive(pc *PeerConnection) { defer t.waitGroup.Done() @@ -948,7 +968,6 @@ func (t *Transport) keepalive(pc *PeerConnection) { } } -// removeConnection removes and cleans up a connection. func (t *Transport) removeConnection(pc *PeerConnection) { t.mutex.Lock() delete(t.connections, pc.Peer.ID) @@ -972,19 +991,28 @@ func (pc *PeerConnection) sendLocked(msg *Message) error { return err } - if err := pc.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + connection := pc.webSocketConnection() + if connection == nil { + return core.E("PeerConnection.Send", "websocket connection is nil", nil) + } + + if err := connection.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { return core.E("PeerConnection.Send", "failed to set write deadline", err) } - defer pc.Conn.SetWriteDeadline(time.Time{}) + defer connection.SetWriteDeadline(time.Time{}) - return pc.Conn.WriteMessage(websocket.BinaryMessage, data) + return connection.WriteMessage(websocket.BinaryMessage, data) } // err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { - err = pc.Conn.Close() + connection := pc.webSocketConnection() + if connection == nil { + return + } + err = connection.Close() }) return err } @@ -1011,6 +1039,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + connection := pc.webSocketConnection() + if connection == nil { + return + } + // Try to send disconnect message (best effort). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() @@ -1027,12 +1060,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } // Close the underlying connection - err = pc.Conn.Close() + err = connection.Close() }) return err } -// encryptMessage encrypts a message using SMSG with the shared secret. func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, error) { // Serialize message to JSON (using pooled buffer for efficiency) msgData, err := MarshalJSON(msg) @@ -1053,7 +1085,6 @@ func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, e return encrypted, nil } -// decryptMessage decrypts a message using SMSG with the shared secret. func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, error) { // Decrypt using shared secret as password password := base64.StdEncoding.EncodeToString(sharedSecret) diff --git a/node/transport_test.go b/node/transport_test.go index 16acd46..5d00ab7 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -562,7 +562,7 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { if clientConn == nil { t.Fatal("client should have connection to server") } - clientConn.Conn.Close() + clientConn.WebSocketConnection.Close() // Wait for server to detect and clean up deadline := time.After(2 * time.Second) @@ -779,7 +779,7 @@ func TestTransport_StartAndStop_Good(t *testing.T) { nm := newTestNodeManager(t, "start-test", RoleWorker) reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() - cfg.ListenAddr = ":0" // Let OS pick a free port + cfg.ListenAddress = ":0" // Let OS pick a free port tr := NewTransport(nm, reg, cfg) diff --git a/specs/node.md b/specs/node.md index 99f6eb8..d00f942 100644 --- a/specs/node.md +++ b/specs/node.md @@ -25,14 +25,14 @@ | `NodeRole` | `type NodeRole string` | Operational mode string for controller, worker, or dual-role nodes. | | `Peer` | `struct{ ID string; Name string; PublicKey string; Address string; Role NodeRole; AddedAt time.Time; LastSeen time.Time; PingMS float64; Hops int; GeoKM float64; Score float64; Connected bool }` | Registry record for a remote node, including addressing, role, scoring metrics, and transient connection state. | | `PeerAuthMode` | `type PeerAuthMode int` | Peer admission policy used by `PeerRegistry` when unknown peers attempt to connect. | -| `PeerConnection` | `struct{ Peer *Peer; Conn *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | +| `PeerConnection` | `struct{ Peer *Peer; WebSocketConnection *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | | `PeerRateLimiter` | `struct{ /* unexported fields */ }` | Per-peer token bucket limiter used by the transport hot path. | | `PeerRegistry` | `struct{ /* unexported fields */ }` | Concurrent peer store with KD-tree selection, allowlist state, and debounced persistence to disk. | | `ProtocolError` | `struct{ Code int; Message string }` | Structured remote error returned by protocol response helpers when a peer replies with `MsgError`. | | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddress string; ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -88,7 +88,7 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddress=:9091`, `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | -- 2.45.3 From 8b33ab2c2b0e8b01542e7389d1882f3ea176ebe2 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 05:22:27 +0000 Subject: [PATCH 60/60] fix(transport): release connection state on close Co-Authored-By: Virgil --- node/transport.go | 67 ++++++++++++++++++++++-------------------- node/transport_test.go | 31 +++++++++++++++++++ 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/node/transport.go b/node/transport.go index 6004aed..08eee04 100644 --- a/node/transport.go +++ b/node/transport.go @@ -969,12 +969,7 @@ func (t *Transport) keepalive(pc *PeerConnection) { } func (t *Transport) removeConnection(pc *PeerConnection) { - t.mutex.Lock() - delete(t.connections, pc.Peer.ID) - t.mutex.Unlock() - - t.peerRegistry.SetConnected(pc.Peer.ID, false) - pc.Close() + _ = pc.Close() } // err := peerConnection.Send(message) @@ -1008,6 +1003,9 @@ func (pc *PeerConnection) sendLocked(msg *Message) error { func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { + if pc.transport != nil { + pc.transport.detachConnection(pc) + } connection := pc.webSocketConnection() if connection == nil { return @@ -1034,35 +1032,40 @@ const ( // err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) func (pc *PeerConnection) GracefulClose(reason string, code int) error { - var err error - pc.closeOnce.Do(func() { - pc.writeMutex.Lock() - defer pc.writeMutex.Unlock() - - connection := pc.webSocketConnection() - if connection == nil { - return - } - - // Try to send disconnect message (best effort). - if pc.transport != nil && pc.SharedSecret != nil { - identity := pc.transport.nodeManager.GetIdentity() - if identity != nil { - payload := DisconnectPayload{ - Reason: reason, - Code: code, - } - msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) - if msgErr == nil { - _ = pc.sendLocked(msg) - } + pc.writeMutex.Lock() + connection := pc.webSocketConnection() + if connection != nil && pc.transport != nil && pc.SharedSecret != nil { + identity := pc.transport.nodeManager.GetIdentity() + if identity != nil { + payload := DisconnectPayload{ + Reason: reason, + Code: code, + } + msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) + if msgErr == nil { + _ = pc.sendLocked(msg) } } + } + pc.writeMutex.Unlock() - // Close the underlying connection - err = connection.Close() - }) - return err + return pc.Close() +} + +func (t *Transport) detachConnection(pc *PeerConnection) { + if pc == nil || pc.Peer == nil { + return + } + + t.mutex.Lock() + current, exists := t.connections[pc.Peer.ID] + if exists && current == pc { + delete(t.connections, pc.Peer.ID) + t.mutex.Unlock() + t.peerRegistry.SetConnected(pc.Peer.ID, false) + return + } + t.mutex.Unlock() } func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, error) { diff --git a/node/transport_test.go b/node/transport_test.go index 5d00ab7..378f8b8 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -619,6 +619,37 @@ func TestTransport_GracefulClose_Ugly(t *testing.T) { } } +func TestTransport_PeerConnectionClose_ReleasesState_Good(t *testing.T) { + tp := setupTestTransportPair(t) + + pc := tp.connectClient(t) + clientID := tp.ClientNode.GetIdentity().ID + if tp.Client.GetConnection(tp.ServerNode.GetIdentity().ID) == nil { + t.Fatal("client should have an active connection before close") + } + + if err := pc.Close(); err != nil { + t.Fatalf("close peer connection: %v", err) + } + + deadline := time.After(2 * time.Second) + for { + select { + case <-deadline: + t.Fatal("connection state was not released after close") + default: + if tp.Client.GetConnection(tp.ServerNode.GetIdentity().ID) == nil && tp.Client.ConnectedPeerCount() == 0 { + peer := tp.ClientReg.GetPeer(clientID) + if peer != nil && peer.Connected { + t.Fatal("registry should show peer as disconnected after close") + } + return + } + time.Sleep(20 * time.Millisecond) + } + } +} + func TestTransport_ConcurrentSends_Ugly(t *testing.T) { tp := setupTestTransportPair(t) -- 2.45.3