mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
215 lines
4.9 KiB
Go
215 lines
4.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package pubsub
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/go-json-experiment/json"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/luxfi/log"
|
|
|
|
"github.com/luxfi/node/pubsub/bloom"
|
|
)
|
|
|
|
var (
|
|
ErrFilterNotInitialized = errors.New("filter not initialized")
|
|
ErrAddressLimit = errors.New("address limit exceeded")
|
|
ErrInvalidFilterParam = errors.New("invalid bloom filter params")
|
|
ErrInvalidCommand = errors.New("invalid command")
|
|
_ Filter = (*connection)(nil)
|
|
)
|
|
|
|
type Filter interface {
|
|
Check(addr []byte) bool
|
|
}
|
|
|
|
// connection is a representation of the websocket connection.
|
|
type connection struct {
|
|
s *Server
|
|
|
|
// The websocket connection.
|
|
conn *websocket.Conn
|
|
|
|
// Buffered channel of outbound messages.
|
|
send chan interface{}
|
|
|
|
fp *FilterParam
|
|
|
|
active uint32
|
|
}
|
|
|
|
func (c *connection) Check(addr []byte) bool {
|
|
return c.fp.Check(addr)
|
|
}
|
|
|
|
func (c *connection) isActive() bool {
|
|
active := atomic.LoadUint32(&c.active)
|
|
return active != 0
|
|
}
|
|
|
|
func (c *connection) deactivate() {
|
|
atomic.StoreUint32(&c.active, 0)
|
|
}
|
|
|
|
func (c *connection) Send(msg interface{}) bool {
|
|
if !c.isActive() {
|
|
return false
|
|
}
|
|
select {
|
|
case c.send <- msg:
|
|
return true
|
|
default:
|
|
}
|
|
return false
|
|
}
|
|
|
|
// readPump pumps messages from the websocket connection to the hub.
|
|
//
|
|
// The application runs readPump in a per-connection goroutine. The application
|
|
// ensures that there is at most one reader on a connection by executing all
|
|
// reads from this goroutine.
|
|
func (c *connection) readPump() {
|
|
defer func() {
|
|
c.deactivate()
|
|
c.s.removeConnection(c)
|
|
|
|
// close is called by both the writePump and the readPump so one of them
|
|
// will always error
|
|
_ = c.conn.Close()
|
|
}()
|
|
|
|
c.conn.SetReadLimit(maxMessageSize)
|
|
// SetReadDeadline returns an error if the connection is corrupted
|
|
if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
|
|
return
|
|
}
|
|
c.conn.SetPongHandler(func(string) error {
|
|
return c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
|
})
|
|
|
|
for {
|
|
err := c.readMessage()
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
c.s.log.Debug("unexpected close in websockets",
|
|
log.Err(err),
|
|
)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// writePump pumps messages from the hub to the websocket connection.
|
|
//
|
|
// A goroutine running writePump is started for each connection. The
|
|
// application ensures that there is at most one writer to a connection by
|
|
// executing all writes from this goroutine.
|
|
func (c *connection) writePump() {
|
|
ticker := time.NewTicker(pingPeriod)
|
|
defer func() {
|
|
c.deactivate()
|
|
ticker.Stop()
|
|
c.s.removeConnection(c)
|
|
|
|
// close is called by both the writePump and the readPump so one of them
|
|
// will always error
|
|
_ = c.conn.Close()
|
|
}()
|
|
for {
|
|
select {
|
|
case message, ok := <-c.send:
|
|
if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
|
|
c.s.log.Debug("closing the connection",
|
|
log.String("reason", "failed to set the write deadline"),
|
|
log.Err(err),
|
|
)
|
|
return
|
|
}
|
|
if !ok {
|
|
// The hub closed the channel. Attempt to close the connection
|
|
// gracefully.
|
|
_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
return
|
|
}
|
|
|
|
if err := c.conn.WriteJSON(message); err != nil {
|
|
return
|
|
}
|
|
case <-ticker.C:
|
|
if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
|
|
c.s.log.Debug("closing the connection",
|
|
log.String("reason", "failed to set the write deadline"),
|
|
log.Err(err),
|
|
)
|
|
return
|
|
}
|
|
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *connection) readMessage() error {
|
|
_, r, err := c.conn.NextReader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := &Command{}
|
|
err = json.UnmarshalRead(r, cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case cmd.NewBloom != nil:
|
|
err = c.handleNewBloom(cmd.NewBloom)
|
|
case cmd.NewSet != nil:
|
|
c.handleNewSet(cmd.NewSet)
|
|
case cmd.AddAddresses != nil:
|
|
err = c.handleAddAddresses(cmd.AddAddresses)
|
|
default:
|
|
err = ErrInvalidCommand
|
|
}
|
|
if err != nil {
|
|
c.Send(&errorMsg{
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *connection) handleNewBloom(cmd *NewBloom) error {
|
|
if !cmd.IsParamsValid() {
|
|
return ErrInvalidFilterParam
|
|
}
|
|
filter, err := bloom.New(int(cmd.MaxElements), float64(cmd.CollisionProb), MaxBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("bloom filter creation failed %w", err)
|
|
}
|
|
c.fp.SetFilter(filter)
|
|
return nil
|
|
}
|
|
|
|
func (c *connection) handleNewSet(_ *NewSet) {
|
|
c.fp.NewSet()
|
|
}
|
|
|
|
func (c *connection) handleAddAddresses(cmd *AddAddresses) error {
|
|
if err := cmd.parseAddresses(); err != nil {
|
|
return fmt.Errorf("address parse failed %w", err)
|
|
}
|
|
err := c.fp.Add(cmd.addressIds...)
|
|
if err != nil {
|
|
return fmt.Errorf("address append failed %w", err)
|
|
}
|
|
c.s.subscribedConnections.Add(c)
|
|
return nil
|
|
}
|