Files
Hanzo AI c3b398bc7b json: migrate every encoding/json import to json/v2 (go-json-experiment)
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)
2026-06-06 22:26:02 -07:00

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
}