VYPR
High severity7.5NVD Advisory· Published Jun 5, 2026

Klever-Go KVM: Hash-array amplification in P2P resolver request handling

CVE-2026-47249

Description

CVE-2026-47249: A flaw in klever-go v1.7.17 allows remote attackers to cause memory and CPU amplification via crafted P2P requests.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2026-47249: A flaw in klever-go v1.7.17 allows remote attackers to cause memory and CPU amplification via crafted P2P requests.

Vulnerability

A vulnerability exists in klever-go versions prior to v1.7.18 where a connected peer can send a compressed RequestDataType_HashArrayType direct request. This request, though small on the wire (442 bytes), expands to 200000 decoded hash entries within the resolver path. The resolver antiflood logic in data/retriever/resolvers/messageProcessor.go [1] and data/batch/batch.go [2] incorrectly accounts for only the compressed size and does not enforce a cap on decoded items. This allows the TxResolver and TrieNodeResolver in data/retriever/resolvers/transactionResolver.go [3] and data/retriever/resolvers/trieNodeResolver.go [4] respectively, to process an excessive number of decoded hashes.

Exploitation

An unauthenticated attacker with network access to a node running a vulnerable version of klever-go can exploit this by sending a specially crafted, compressed RequestDataType_HashArrayType direct request. This request is designed to be small on the wire but to decompress into a large number of hash entries. The node then attempts to process these entries, leading to resource exhaustion.

Impact

Successful exploitation of this vulnerability allows a remote attacker to cause significant memory and CPU amplification on the target node. This can lead to a denial-of-service (DoS) condition, making the node unresponsive and potentially disrupting the network. The impact is limited to resource exhaustion and does not grant unauthorized access or data modification.

Mitigation

The vulnerability is fixed in klever-go version v1.7.18, released on 2026-06-05 [1]. This release caps the decoded item count in Batch.Decompress to 8192 items, addressing the amplification gap in the resolver paths. All node operators should upgrade to v1.7.18 or later promptly. No workarounds are available for earlier versions.

AI Insight generated on Jun 5, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

7
2cf19442b62c

[KLC-2427] p2p: process direct-message ingress synchronously (GHSA-hf2g-6j7h-98wg)

https://github.com/klever-io/klever-goFernando SobreiraMay 30, 2026Fixed in 1.7.18via ghsa-release-walk
5 files changed · +361 36
  • data/retriever/storageResolvers/selfSend_importdb_test.go+190 0 added
    @@ -0,0 +1,190 @@
    +package storageResolvers
    +
    +import (
    +	"bytes"
    +	"fmt"
    +	"sync"
    +	"sync/atomic"
    +	"testing"
    +	"time"
    +
    +	commonMock "github.com/klever-io/klever-go/common/mock"
    +	"github.com/klever-io/klever-go/config"
    +	"github.com/klever-io/klever-go/core"
    +	"github.com/klever-io/klever-go/data/batch"
    +	"github.com/klever-io/klever-go/data/endProcess"
    +	"github.com/klever-io/klever-go/network/p2p"
    +	"github.com/klever-io/klever-go/network/p2p/libp2p"
    +	p2pMock "github.com/klever-io/klever-go/network/p2p/mock"
    +	mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +// selfSendTimeout bounds each self-send so a deadlock regression fails fast instead of hanging.
    +const selfSendTimeout = 2 * time.Second
    +
    +// newLoopbackMessenger builds a real in-memory networkMessenger to exercise the genuine
    +// SendToConnectedPeer(self) -> sendDirectToSelf -> directMessageHandler loopback.
    +func newLoopbackMessenger(t *testing.T) p2p.Messenger {
    +	t.Helper()
    +
    +	mes, err := libp2p.NewMockMessenger(
    +		libp2p.ArgsNetworkMessenger{
    +			Marshalizer:   &commonMock.ProtoMarshalizerMock{},
    +			ListenAddress: libp2p.ListenLocalhostAddrWithIp4AndTcp,
    +			P2pConfig: config.P2PConfig{
    +				Node:                config.NodeConfig{Port: "0"},
    +				KadDhtPeerDiscovery: config.KadDhtPeerDiscoveryConfig{Enabled: false},
    +				Sharding:            config.ShardingConfig{Type: p2p.NilListSharder},
    +			},
    +			SyncTimer: &libp2p.LocalSyncTimer{},
    +		},
    +		mocknet.New(),
    +	)
    +	require.Nil(t, err)
    +
    +	return mes
    +}
    +
    +// runWithin runs fn and fails the test if it does not return before selfSendTimeout.
    +func runWithin(t *testing.T, what string, fn func() error) error {
    +	t.Helper()
    +
    +	done := make(chan error, 1)
    +	go func() {
    +		// Turn a panic in fn into a clean test failure instead of crashing the test binary.
    +		defer func() {
    +			if r := recover(); r != nil {
    +				done <- fmt.Errorf("%s panicked: %v", what, r)
    +			}
    +		}()
    +		done <- fn()
    +	}()
    +
    +	select {
    +	case err := <-done:
    +		return err
    +	case <-time.After(selfSendTimeout):
    +		// On timeout the worker goroutine is intentionally leaked: this is a fail-fast guard and
    +		// the process exits via Fatalf, so the leak does not outlive the test.
    +		t.Fatalf("%s deadlocked: self-send did not return within %s — import-db replay would hang", what, selfSendTimeout)
    +		return nil
    +	}
    +}
    +
    +// TestSliceResolver_RequestDataFromHash_SelfSendIsSynchronous guards the import-db replay path:
    +// storage resolvers answer local requests via sendToSelf -> SendToConnectedPeer(self). After the
    +// GHSA-hf2g-6j7h-98wg fix this must deliver to the response-topic processor synchronously (already
    +// processed by the time RequestDataFromHash returns), with no error and no deadlock.
    +func TestSliceResolver_RequestDataFromHash_SelfSendIsSynchronous(t *testing.T) {
    +	// Not t.Parallel: messenger construction races on the libp2p global pubsub.TimeCacheDuration
    +	// (KLC-2430), so two messengers cannot be built concurrently.
    +	const responseTopic = "txBlockBodies_0_RESPONSE"
    +	hash := []byte("mb-hash")
    +	value := []byte("marshalled-miniblock-bytes")
    +
    +	mes := newLoopbackMessenger(t)
    +	defer func() { _ = mes.Close() }()
    +
    +	marshalizer := &commonMock.MarshalizerMock{}
    +
    +	var processed int32
    +	var received atomic.Value // []byte
    +	err := mes.RegisterMessageProcessor(responseTopic, &p2pMock.MessageProcessorStub{
    +		ProcessMessageCalled: func(msg p2p.MessageP2P, _ core.PeerID) error {
    +			received.Store(append([]byte(nil), msg.Data()...))
    +			atomic.AddInt32(&processed, 1)
    +			return nil
    +		},
    +	})
    +	require.Nil(t, err)
    +
    +	storer := commonMock.NewStorerMock("Storage", 0)
    +	require.Nil(t, storer.Put(hash, value))
    +
    +	arg := ArgSliceResolver{
    +		Messenger:                mes,
    +		ResponseTopicName:        responseTopic,
    +		Storage:                  storer,
    +		DataPacker:               &commonMock.DataPackerStub{},
    +		Marshalizer:              marshalizer,
    +		ManualEpochStartNotifier: &commonMock.ManualEpochStartNotifierStub{},
    +		ChanGracefullyClose:      make(chan endProcess.ArgEndProcess, 1),
    +	}
    +	res, err := NewSliceResolver(arg)
    +	require.Nil(t, err)
    +
    +	err = runWithin(t, "RequestDataFromHash", func() error {
    +		return res.RequestDataFromHash(hash, 0)
    +	})
    +	require.Nil(t, err)
    +
    +	// Synchronous: processing must be done by the time RequestDataFromHash returns.
    +	require.Equal(t, int32(1), atomic.LoadInt32(&processed),
    +		"self-sent response must be processed synchronously before RequestDataFromHash returns")
    +
    +	// Payload must match the resolver's marshalled batch.
    +	expected, err := marshalizer.Marshal(&batch.Batch{Data: [][]byte{value}})
    +	require.Nil(t, err)
    +	got, _ := received.Load().([]byte)
    +	require.True(t, bytes.Equal(expected, got),
    +		"delivered payload must match the resolver's marshalled batch")
    +}
    +
    +// TestSliceResolver_RequestDataFromHashArray_SelfSendDeliversEveryChunk guards the multi-message
    +// self-send loop (one sendToSelf per packed chunk): every chunk must arrive synchronously, in order.
    +func TestSliceResolver_RequestDataFromHashArray_SelfSendDeliversEveryChunk(t *testing.T) {
    +	// Not t.Parallel: see TestSliceResolver_RequestDataFromHash_SelfSendIsSynchronous.
    +	const responseTopic = "miniBlocks_0_RESPONSE"
    +	hashes := [][]byte{[]byte("h1"), []byte("h2"), []byte("h3")}
    +	values := [][]byte{[]byte("v1"), []byte("v2"), []byte("v3")}
    +
    +	mes := newLoopbackMessenger(t)
    +	defer func() { _ = mes.Close() }()
    +
    +	var mut sync.Mutex
    +	deliveries := make([][]byte, 0, len(hashes))
    +	err := mes.RegisterMessageProcessor(responseTopic, &p2pMock.MessageProcessorStub{
    +		ProcessMessageCalled: func(msg p2p.MessageP2P, _ core.PeerID) error {
    +			mut.Lock()
    +			deliveries = append(deliveries, append([]byte(nil), msg.Data()...))
    +			mut.Unlock()
    +			return nil
    +		},
    +	})
    +	require.Nil(t, err)
    +
    +	storer := commonMock.NewStorerMock("Storage", 0)
    +	for i := range hashes {
    +		require.Nil(t, storer.Put(hashes[i], values[i]))
    +	}
    +
    +	// Identity packer: one chunk per stored value, preserving order.
    +	packer := &commonMock.DataPackerStub{
    +		PackDataInChunksCalled: func(data [][]byte, _ int) ([][]byte, error) {
    +			return data, nil
    +		},
    +	}
    +
    +	arg := ArgSliceResolver{
    +		Messenger:                mes,
    +		ResponseTopicName:        responseTopic,
    +		Storage:                  storer,
    +		DataPacker:               packer,
    +		Marshalizer:              &commonMock.MarshalizerMock{},
    +		ManualEpochStartNotifier: &commonMock.ManualEpochStartNotifierStub{},
    +		ChanGracefullyClose:      make(chan endProcess.ArgEndProcess, 1),
    +	}
    +	res, err := NewSliceResolver(arg)
    +	require.Nil(t, err)
    +
    +	err = runWithin(t, "RequestDataFromHashArray", func() error {
    +		return res.RequestDataFromHashArray(hashes, 0)
    +	})
    +	require.Nil(t, err)
    +
    +	mut.Lock()
    +	defer mut.Unlock()
    +	require.Equal(t, values, deliveries,
    +		"every self-sent chunk must be delivered synchronously and in order")
    +}
    
  • network/p2p/libp2p/directSender.go+9 0 modified
    @@ -27,6 +27,10 @@ var _ p2p.DirectSender = (*directSender)(nil)
     const timeSeenMessages = time.Second * 120
     const maxMutexes = 10000
     
    +// directSendWriteTimeout bounds a single direct-message write so a stalled peer cannot pin the
    +// writer. 10s covers legitimate ~1 MiB responses. See GHSA-hf2g-6j7h-98wg.
    +const directSendWriteTimeout = time.Second * 10
    +
     type directSender struct {
     	counter         uint64
     	ctx             context.Context
    @@ -170,6 +174,8 @@ func (ds *directSender) Send(topic string, buff []byte, peer core.PeerID) error
     		return err
     	}
     
    +	_ = stream.SetWriteDeadline(time.Now().Add(directSendWriteTimeout))
    +
     	msg := ds.createMessage(topic, buff, conn)
     
     	bufw := bufio.NewWriter(stream)
    @@ -189,6 +195,9 @@ func (ds *directSender) Send(topic string, buff []byte, peer core.PeerID) error
     		return err
     	}
     
    +	// Clear the deadline on the reused stream.
    +	_ = stream.SetWriteDeadline(time.Time{})
    +
     	return nil
     }
     
    
  • network/p2p/libp2p/dos_directmsg_test.go+116 0 added
    @@ -0,0 +1,116 @@
    +package libp2p_test
    +
    +import (
    +	"fmt"
    +	"sync"
    +	"sync/atomic"
    +	"testing"
    +	"time"
    +
    +	"github.com/klever-io/klever-go/core"
    +	"github.com/klever-io/klever-go/network/p2p"
    +	"github.com/klever-io/klever-go/network/p2p/libp2p"
    +	"github.com/klever-io/klever-go/network/p2p/mock"
    +	mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +// TestNetworkMessenger_DirectMessageHandler_ProcessesSynchronously guards GHSA-hf2g-6j7h-98wg:
    +// direct-message processing must be synchronous, so in-flight processing is bounded by the number
    +// of streams, not the number of messages. N senders (=> N streams) flood one receiver whose
    +// processor parks until released; the test asserts peak in-flight stays at N (pre-fix it would
    +// approach the message count, as each message spawned its own goroutine).
    +func TestNetworkMessenger_DirectMessageHandler_ProcessesSynchronously(t *testing.T) {
    +	const numSenders = 4
    +	const perSender = 25 // 100 messages total; pre-fix this would approach 100 in-flight
    +
    +	netw := mocknet.New()
    +
    +	receiver, err := libp2p.NewMockMessenger(createMockNetworkArgs(), netw)
    +	require.Nil(t, err)
    +	defer func() { _ = receiver.Close() }()
    +
    +	senders := make([]p2p.Messenger, numSenders)
    +	for i := range senders {
    +		s, errS := libp2p.NewMockMessenger(createMockNetworkArgs(), netw)
    +		require.Nil(t, errS)
    +		senders[i] = s
    +		defer func(m p2p.Messenger) { _ = m.Close() }(s)
    +	}
    +
    +	require.Nil(t, netw.LinkAll())
    +
    +	var current, maxObserved, exceeded int32
    +	release := make(chan struct{})
    +
    +	err = receiver.RegisterMessageProcessor("test", &mock.MessageProcessorStub{
    +		ProcessMessageCalled: func(_ p2p.MessageP2P, _ core.PeerID) error {
    +			c := atomic.AddInt32(&current, 1)
    +			for {
    +				m := atomic.LoadInt32(&maxObserved)
    +				if c <= m || atomic.CompareAndSwapInt32(&maxObserved, m, c) {
    +					break
    +				}
    +			}
    +			if c > int32(numSenders) {
    +				atomic.StoreInt32(&exceeded, 1)
    +			}
    +			<-release // park, keeping every concurrently-processing message in-flight
    +			atomic.AddInt32(&current, -1)
    +			return nil
    +		},
    +	})
    +	require.Nil(t, err)
    +
    +	for _, s := range senders {
    +		require.Nil(t, s.ConnectToPeer(receiver.Addresses()[0]))
    +	}
    +
    +	// Wait until every sender is connected before flooding (polling beats a fixed sleep, which
    +	// flakes under load). A direct send needs a live connection.
    +	connectDeadline := time.Now().Add(5 * time.Second)
    +	for _, s := range senders {
    +		for !s.IsConnected(receiver.ID()) {
    +			require.True(t, time.Now().Before(connectDeadline),
    +				"senders did not connect to the receiver within the deadline")
    +			time.Sleep(10 * time.Millisecond)
    +		}
    +	}
    +
    +	// Each sender floods on its own goroutine. With a parked processor, only the first message per
    +	// stream is consumed; the rest block on backpressure rather than soaking into goroutines.
    +	var wg sync.WaitGroup
    +	for _, s := range senders {
    +		wg.Add(1)
    +		go func(m p2p.Messenger) {
    +			defer wg.Done()
    +			for j := 0; j < perSender; j++ {
    +				_ = m.SendToConnectedPeer("test", []byte(fmt.Sprintf("m-%d", j)), receiver.ID())
    +			}
    +		}(s)
    +	}
    +
    +	// Wait until one message per stream is parked, asserting in-flight never exceeds stream count.
    +	reached := false
    +	deadline := time.Now().Add(5 * time.Second)
    +	for time.Now().Before(deadline) {
    +		require.Zero(t, atomic.LoadInt32(&exceeded),
    +			"in-flight exceeded live-stream count — per-message goroutine spawn has regressed")
    +		if atomic.LoadInt32(&current) == int32(numSenders) {
    +			reached = true
    +			break
    +		}
    +		time.Sleep(20 * time.Millisecond)
    +	}
    +	require.True(t, reached,
    +		"in-flight never reached the stream count (%d); sent %d total", numSenders, numSenders*perSender)
    +
    +	// Hold to confirm in-flight does NOT climb toward the message count.
    +	time.Sleep(300 * time.Millisecond)
    +	require.LessOrEqual(t, atomic.LoadInt32(&maxObserved), int32(numSenders),
    +		"peak in-flight must stay at stream count (%d), not message count (%d)",
    +		numSenders, numSenders*perSender)
    +
    +	close(release) // unblock parked processors; backpressured sends now drain
    +	wg.Wait()
    +}
    
  • network/p2p/libp2p/netMessenger.go+43 33 modified
    @@ -1131,10 +1131,30 @@ func (netMes *networkMessenger) sendDirectToSelf(topic string, buff []byte) erro
     	return netMes.directMessageHandler(msg, netMes.ID())
     }
     
    -func (netMes *networkMessenger) directMessageHandler(message *pubsub.Message, fromConnectedPeer core.PeerID) error {
    +func (netMes *networkMessenger) directMessageHandler(message *pubsub.Message, fromConnectedPeer core.PeerID) (err error) {
     	var processor p2p.MessageProcessor
     
    +	// Guard before dereferencing: callers validate this today, but the deref must not panic
    +	// outside the recover below, or it would crash the node it is meant to protect.
    +	if message == nil || message.Topic == nil {
    +		return p2p.ErrNilMessage
    +	}
     	topic := *message.Topic
    +
    +	// Recover so a panic on untrusted data cannot crash the node: this now runs synchronously
    +	// in the stream reader goroutine, which has no recover of its own (GHSA-rm5c-5x2p-48wr).
    +	defer func() {
    +		if r := recover(); r != nil {
    +			netMes.recoverFromMessagePanic(r, topic, fromConnectedPeer, message)
    +			dataLen := 0
    +			if message != nil {
    +				dataLen = len(message.GetData())
    +			}
    +			netMes.processDebugMessage(topic, fromConnectedPeer, uint64(dataLen), true)
    +			err = nil
    +		}
    +	}()
    +
     	msg, err := netMes.transformAndCheckMessage(message, fromConnectedPeer, topic)
     	if err != nil {
     		return err
    @@ -1148,38 +1168,28 @@ func (netMes *networkMessenger) directMessageHandler(message *pubsub.Message, fr
     		return fmt.Errorf("%w on directMessageHandler for topic %s", p2p.ErrNilValidator, topic)
     	}
     
    -	go func(msg p2p.MessageP2P) {
    -		// Circuit-breaker for the direct-send path: the gossip recover does not cover
    -		// this goroutine (GHSA-rm5c-5x2p-48wr).
    -		defer func() {
    -			if r := recover(); r != nil {
    -				netMes.recoverFromMessagePanic(r, topic, fromConnectedPeer, message)
    -				dataLen := 0
    -				if message != nil {
    -					dataLen = len(message.GetData())
    -				}
    -				netMes.processDebugMessage(topic, fromConnectedPeer, uint64(dataLen), true)
    -			}
    -		}()
    -
    -		if check.IfNil(msg) {
    -			return
    -		}
    -
    -		//we won't recheck the message id against the cacher here as there might be collisions since we are using
    -		// a separate sequence counter for direct sender
    -		errProcess := processor.ProcessReceivedMessage(msg, fromConnectedPeer)
    -		if errProcess != nil {
    -			log.Trace("p2p validator directMessageHandler",
    -				"error", errProcess.Error(),
    -				"topic", msg.Topic(),
    -				"originator", p2p.MessageOriginatorPid(msg),
    -				"from connected peer", p2p.PeerIDToShortString(fromConnectedPeer),
    -				"seq no", p2p.MessageOriginatorSeq(msg),
    -			)
    -		}
    -		netMes.debugger.AddIncomingMessage(msg.Topic(), uint64(len(msg.Data())), errProcess != nil)
    -	}(msg)
    +	if check.IfNil(msg) {
    +		return p2p.ErrNilMessage
    +	}
    +
    +	// Process synchronously: the per-stream reader bounds concurrency to one in-flight per stream,
    +	// instead of one goroutine per message which a single peer could grow unbounded (GHSA-hf2g-6j7h-98wg).
    +	// Trade-off: a slow processor head-of-line-blocks its own stream, so direct-send processors
    +	// must stay non-blocking.
    +	//
    +	//we won't recheck the message id against the cacher here as there might be collisions since we are using
    +	// a separate sequence counter for direct sender
    +	errProcess := processor.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	if errProcess != nil {
    +		log.Trace("p2p validator directMessageHandler",
    +			"error", errProcess.Error(),
    +			"topic", msg.Topic(),
    +			"originator", p2p.MessageOriginatorPid(msg),
    +			"from connected peer", p2p.PeerIDToShortString(fromConnectedPeer),
    +			"seq no", p2p.MessageOriginatorSeq(msg),
    +		)
    +	}
    +	netMes.debugger.AddIncomingMessage(msg.Topic(), uint64(len(msg.Data())), errProcess != nil)
     
     	return nil
     }
    
  • network/p2p/mock/streamMock.go+3 3 modified
    @@ -86,17 +86,17 @@ func (sm *streamMock) Reset() error {
     
     // SetDeadline -
     func (sm *streamMock) SetDeadline(time.Time) error {
    -	panic("implement me")
    +	return nil
     }
     
     // SetReadDeadline -
     func (sm *streamMock) SetReadDeadline(time.Time) error {
    -	panic("implement me")
    +	return nil
     }
     
     // SetWriteDeadline -
     func (sm *streamMock) SetWriteDeadline(time.Time) error {
    -	panic("implement me")
    +	return nil
     }
     
     // Protocol -
    
a3c2e67ac6a8

[KLC-2432] fix nil-Header panic in block interceptor + cover direct-send recover gap (GHSA-rm5c-5x2p-48wr)

https://github.com/klever-io/klever-goFernando SobreiraMay 30, 2026Fixed in 1.7.18via ghsa-release-walk
5 files changed · +162 22
  • core/process/block/interceptedBlocks/interceptedBlock.go+14 5 modified
    @@ -149,18 +149,27 @@ func (inMb *InterceptedBlock) Type() string {
     
     // String returns the transactions body's most important fields as string
     func (inMb *InterceptedBlock) String() string {
    +	// Runs pre-CheckValidity on untrusted data; guard block + nil-safe getters so an
    +	// omitted Header cannot panic (GHSA-rm5c-5x2p-48wr).
    +	if inMb.block == nil {
    +		return ""
    +	}
     	return fmt.Sprintf("epoch=%d, slot=%d, nonce=%d, block numTxs=%d",
    -		inMb.block.Header.Epoch,
    -		inMb.block.Header.Slot,
    -		inMb.block.Header.Nonce,
    +		inMb.block.GetEpoch(),
    +		inMb.block.GetSlot(),
    +		inMb.block.GetNonce(),
     		len(inMb.block.TxHashes),
     	)
     }
     
     // Identifiers returns the identifiers used in requests
     func (inMb *InterceptedBlock) Identifiers() [][]byte {
    -	keyNonce := []byte(fmt.Sprintf("nonce-%d", inMb.block.Header.Nonce))
    -	keyEpoch := []byte(core.EpochStartIdentifier(inMb.block.Header.Epoch))
    +	// Same guard as String: runs pre-CheckValidity on untrusted data (GHSA-rm5c-5x2p-48wr).
    +	if inMb.block == nil {
    +		return [][]byte{inMb.hash}
    +	}
    +	keyNonce := []byte(fmt.Sprintf("nonce-%d", inMb.block.GetNonce()))
    +	keyEpoch := []byte(core.EpochStartIdentifier(inMb.block.GetEpoch()))
     
     	return [][]byte{inMb.hash, keyNonce, keyEpoch}
     }
    
  • core/process/block/interceptedBlocks/interceptedBlock_test.go+27 0 modified
    @@ -437,3 +437,30 @@ func TestInterceptedBlock_CheckValidity(t *testing.T) {
     	}
     
     }
    +
    +// Regression test for GHSA-rm5c-5x2p-48wr (block variant): a nil Header must not panic
    +// Identifiers()/String() (they run before CheckValidity, which still rejects the block).
    +func TestInterceptedBlock_NilHeaderIdentifiersAndStringShouldNotPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createDefaultBlockArgument()
    +	// Use the proto marshaler to mirror the real gossip/direct-send wire path. Header
    +	// omitted — stays nil after unmarshal, mirroring the PoC payload.
    +	protoMarshalizer := &mock.ProtoMarshalizerMock{}
    +	arg.Marshalizer = protoMarshalizer
    +	arg.BlockBuff, _ = protoMarshalizer.Marshal(&block.Block{PubKeysBitmap: []byte{1}})
    +
    +	inBlk, err := interceptedBlocks.NewInterceptedBlock(arg)
    +	require.Nil(t, err)
    +	require.False(t, check.IfNil(inBlk))
    +
    +	require.NotPanics(t, func() {
    +		_ = inBlk.Identifiers()
    +		_ = inBlk.String()
    +	})
    +
    +	// CheckValidity must still reject the malformed (nil-header) block.
    +	err = inBlk.CheckValidity()
    +	require.Error(t, err)
    +	assert.ErrorIs(t, err, common.ErrNilPreviousBlockHash)
    +}
    
  • network/p2p/libp2p/export_test.go+5 0 modified
    @@ -3,6 +3,7 @@ package libp2p
     import (
     	"context"
     
    +	"github.com/klever-io/klever-go/core"
     	"github.com/klever-io/klever-go/network/p2p"
     	"github.com/klever-io/klever-go/storage"
     	pubsub "github.com/libp2p/go-libp2p-pubsub"
    @@ -41,6 +42,10 @@ func (netMes *networkMessenger) PubsubCallback(handler p2p.MessageProcessor, top
     	return netMes.pubsubCallback(handler, topic)
     }
     
    +func (netMes *networkMessenger) DirectMessageHandler(message *pubsub.Message, fromConnectedPeer core.PeerID) error {
    +	return netMes.directMessageHandler(message, fromConnectedPeer)
    +}
    +
     func (netMes *networkMessenger) ValidMessageByTimestamp(msg p2p.MessageP2P) error {
     	return netMes.validMessageByTimestamp(msg)
     }
    
  • network/p2p/libp2p/netMessenger.go+37 17 modified
    @@ -892,28 +892,15 @@ func (netMes *networkMessenger) pubsubCallback(handler p2p.MessageProcessor, top
     	return func(ctx context.Context, pid peer.ID, message *pubsub.Message) (isValid bool) {
     		fromConnectedPeer := core.PeerID(pid)
     
    -		// Circuit-breaker: this callback runs in an unrecovered pubsub goroutine, so a
    -		// panic on an untrusted message would crash the node (GHSA-rm5c-5x2p-48wr).
    +		// Circuit-breaker: recover so a panic on an untrusted message cannot crash the
    +		// node (GHSA-rm5c-5x2p-48wr).
     		defer func() {
     			if r := recover(); r != nil {
    -				// Read message via nil-safe getters: the recover must not panic itself.
    -				var dataLen int
    -				var originator core.PeerID
    +				netMes.recoverFromMessagePanic(r, topic, fromConnectedPeer, message)
    +				dataLen := 0
     				if message != nil {
     					dataLen = len(message.GetData())
    -					originator = core.PeerID(message.GetFrom())
     				}
    -
    -				log.Error("recovered from panic in pubsubCallback",
    -					"topic", topic,
    -					"originator", p2p.PeerIDToShortString(originator),
    -					"from connected peer", p2p.PeerIDToShortString(fromConnectedPeer),
    -					"panic", fmt.Sprintf("%v", r),
    -					"stack", string(debug.Stack()),
    -				)
    -				// Blacklist both peers, as transformAndCheckMessage does on severe errors.
    -				netMes.blacklistPid(fromConnectedPeer, core.WrongP2PMessageBlacklistDuration)
    -				netMes.blacklistPid(originator, core.WrongP2PMessageBlacklistDuration)
     				netMes.processDebugMessage(topic, fromConnectedPeer, uint64(dataLen), true)
     				isValid = false
     			}
    @@ -943,6 +930,26 @@ func (netMes *networkMessenger) pubsubCallback(handler p2p.MessageProcessor, top
     	}
     }
     
    +// recoverFromMessagePanic logs a recovered panic from processing an untrusted p2p
    +// message and blacklists both the connected peer and the originator, using nil-safe
    +// accessors. Shared by the gossip and direct-send receive paths (GHSA-rm5c-5x2p-48wr).
    +func (netMes *networkMessenger) recoverFromMessagePanic(r interface{}, topic string, fromConnectedPeer core.PeerID, message *pubsub.Message) {
    +	var originator core.PeerID
    +	if message != nil {
    +		originator = core.PeerID(message.GetFrom())
    +	}
    +
    +	log.Error("recovered from panic while processing p2p message",
    +		"topic", topic,
    +		"originator", p2p.PeerIDToShortString(originator),
    +		"from connected peer", p2p.PeerIDToShortString(fromConnectedPeer),
    +		"panic", fmt.Sprintf("%v", r),
    +		"stack", string(debug.Stack()),
    +	)
    +	netMes.blacklistPid(fromConnectedPeer, core.WrongP2PMessageBlacklistDuration)
    +	netMes.blacklistPid(originator, core.WrongP2PMessageBlacklistDuration)
    +}
    +
     func (netMes *networkMessenger) transformAndCheckMessage(pbMsg *pubsub.Message, pid core.PeerID, topic string) (p2p.MessageP2P, error) {
     	msg, errUnmarshal := NewMessage(pbMsg, netMes.marshalizer)
     	if errUnmarshal != nil {
    @@ -1142,6 +1149,19 @@ func (netMes *networkMessenger) directMessageHandler(message *pubsub.Message, fr
     	}
     
     	go func(msg p2p.MessageP2P) {
    +		// Circuit-breaker for the direct-send path: the gossip recover does not cover
    +		// this goroutine (GHSA-rm5c-5x2p-48wr).
    +		defer func() {
    +			if r := recover(); r != nil {
    +				netMes.recoverFromMessagePanic(r, topic, fromConnectedPeer, message)
    +				dataLen := 0
    +				if message != nil {
    +					dataLen = len(message.GetData())
    +				}
    +				netMes.processDebugMessage(topic, fromConnectedPeer, uint64(dataLen), true)
    +			}
    +		}()
    +
     		if check.IfNil(msg) {
     			return
     		}
    
  • network/p2p/libp2p/netMessenger_test.go+79 0 modified
    @@ -1599,6 +1599,85 @@ func TestNetworkMessenger_PubsubCallbackRecoversFromHandlerPanic(t *testing.T) {
     	_ = mes.Close()
     }
     
    +// Regression test for GHSA-rm5c-5x2p-48wr (direct-send variant): a panic in the
    +// direct-send goroutine must be recovered (no crash) and blacklist the sender.
    +func TestNetworkMessenger_DirectMessageHandlerRecoversFromPanic(t *testing.T) {
    +	args := libp2p.ArgsNetworkMessenger{
    +		Marshalizer:   &commonMock.ProtoMarshalizerMock{},
    +		ListenAddress: libp2p.ListenLocalhostAddrWithIp4AndTcp,
    +		P2pConfig: config.P2PConfig{
    +			Node: config.NodeConfig{
    +				Port: "0",
    +			},
    +			KadDhtPeerDiscovery: config.KadDhtPeerDiscoveryConfig{
    +				Enabled: false,
    +			},
    +			Sharding: config.ShardingConfig{
    +				Type: p2p.NilListSharder,
    +			},
    +		},
    +		SyncTimer: &libp2p.LocalSyncTimer{},
    +	}
    +
    +	mes, _ := libp2p.NewNetworkMessenger(args)
    +
    +	// processReceivedDirectMessage enforces From == fromConnectedPeer, so the originator
    +	// and connected peer are the same on the direct path.
    +	peerID := mes.ID()
    +
    +	blacklistedCh := make(chan string, 4)
    +	_ = mes.SetPeerDenialEvaluator(&mock.PeerDenialEvaluatorStub{
    +		UpsertPeerIDCalled: func(pid core.PeerID, duration time.Duration) error {
    +			blacklistedCh <- string(pid)
    +			return nil
    +		},
    +		IsDeniedCalled: func(pid core.PeerID) bool {
    +			return false
    +		},
    +	})
    +
    +	topic := "topic"
    +	_ = mes.CreateTopic(topic, false)
    +	numCalled := uint32(0)
    +	_ = mes.RegisterMessageProcessor(topic, &mock.MessageProcessorStub{
    +		ProcessMessageCalled: func(message p2p.MessageP2P, fromConnectedPeer core.PeerID) error {
    +			atomic.AddUint32(&numCalled, 1)
    +			panic("simulated nil-pointer dereference while processing a malicious block")
    +		},
    +	})
    +
    +	innerMessage := &data.TopicMessage{
    +		Payload:   []byte("data"),
    +		Timestamp: time.Now().Unix(),
    +		Version:   libp2p.CurrentTopicMessageVersion,
    +	}
    +	buff, _ := args.Marshalizer.Marshal(innerMessage)
    +	msg := &pubsub.Message{
    +		Message: &pubsub_pb.Message{
    +			From:  []byte(peerID),
    +			Data:  buff,
    +			Seqno: []byte{0, 0, 0, 1},
    +			Topic: &topic,
    +		},
    +	}
    +
    +	// The recover runs in a goroutine: if it failed, the test binary would crash.
    +	require.NotPanics(t, func() {
    +		_ = mes.DirectMessageHandler(msg, peerID)
    +	})
    +
    +	// Wait for the recover to blacklist the sender.
    +	select {
    +	case pid := <-blacklistedCh:
    +		assert.Equal(t, string(peerID), pid, "sender should be blacklisted on panic")
    +	case <-time.After(2 * time.Second):
    +		t.Fatal("timed out waiting for the sender to be blacklisted")
    +	}
    +	assert.Equal(t, uint32(1), atomic.LoadUint32(&numCalled))
    +
    +	_ = mes.Close()
    +}
    +
     func TestNetworkMessenger_UnjoinAllTopicsShouldWork(t *testing.T) {
     	args := libp2p.ArgsNetworkMessenger{
     		Marshalizer:   &commonMock.ProtoMarshalizerMock{},
    
7622deed6128

[KLC-2432] fix unauthenticated nil-pointer panic in tx interceptor (GHSA-rm5c-5x2p-48wr)

https://github.com/klever-io/klever-goFernando SobreiraMay 29, 2026Fixed in 1.7.18via ghsa-release-walk
6 files changed · +183 1
  • core/process/transaction/interceptedTransaction.go+6 0 modified
    @@ -146,6 +146,12 @@ func createTx(marshalizer marshal.Marshalizer, txBuff []byte) (*transaction.Tran
     		return nil, err
     	}
     
    +	// A Transaction with RawData omitted unmarshals to a nil RawData; reject it so
    +	// downstream code can rely on RawData being set (GHSA-rm5c-5x2p-48wr).
    +	if tx.RawData == nil {
    +		return nil, process.ErrNilTransaction
    +	}
    +
     	return tx, nil
     }
     
    
  • core/process/transaction/interceptedTransaction_test.go+18 0 modified
    @@ -513,6 +513,24 @@ func TestNewInterceptedTransaction_UnmarshalingTxFailsShouldErr(t *testing.T) {
     	assert.Equal(t, errExpected, err)
     }
     
    +// Regression test for GHSA-rm5c-5x2p-48wr: the constructor must reject a tx with
    +// nil RawData instead of letting it panic the node later in CheckValidity.
    +func TestNewInterceptedTransaction_NilRawDataShouldErrAndNotPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	chainID := []byte("chain")
    +	// RawData omitted — stays nil after unmarshal, mirroring the PoC payload.
    +	tx := &dataTransaction.Transaction{
    +		Signature: [][]byte{{0x78}},
    +	}
    +
    +	require.NotPanics(t, func() {
    +		txi, err := createInterceptedTxFromPlainTx(tx, createFreeTxFeeHandler(), chainID, 1)
    +		assert.True(t, check.IfNil(txi))
    +		assert.Equal(t, process.ErrNilTransaction, err)
    +	})
    +}
    +
     func TestNewInterceptedTransaction_ShouldWork(t *testing.T) {
     	t.Parallel()
     
    
  • core/versioning/txVersionChecker.go+6 0 modified
    @@ -19,6 +19,12 @@ func NewTxVersionChecker(minTxVersion uint32) *txVersionChecker {
     
     // CheckTxVersion will check transaction version
     func (tvc *txVersionChecker) CheckTxVersion(tx *transaction.Transaction) error {
    +	// Must stay ErrInvalidTransactionVersion: IsWrongVersionError keys on it to
    +	// blacklist the sender.
    +	if tx == nil || tx.RawData == nil {
    +		return process.ErrInvalidTransactionVersion
    +	}
    +
     	if tx.RawData.Version < tvc.minTxVersion {
     		return process.ErrInvalidTransactionVersion
     	}
    
  • core/versioning/txVersionChecker_test.go+32 0 modified
    @@ -3,6 +3,7 @@ package versioning
     import (
     	"testing"
     
    +	"github.com/klever-io/klever-go/core/process"
     	"github.com/klever-io/klever-go/data/transaction"
     	"github.com/stretchr/testify/require"
     )
    @@ -18,3 +19,34 @@ func TestTxVersionChecker_CheckTxVersionShouldWork(t *testing.T) {
     	err := tvc.CheckTxVersion(tx)
     	require.Nil(t, err)
     }
    +
    +// Regression test for GHSA-rm5c-5x2p-48wr: nil RawData must error, not panic.
    +func TestTxVersionChecker_CheckTxVersionNilRawDataShouldErrAndNotPanic(t *testing.T) {
    +	tvc := NewTxVersionChecker(1)
    +
    +	require.NotPanics(t, func() {
    +		err := tvc.CheckTxVersion(&transaction.Transaction{})
    +		require.Equal(t, process.ErrInvalidTransactionVersion, err)
    +	})
    +}
    +
    +func TestTxVersionChecker_CheckTxVersionNilTxShouldErrAndNotPanic(t *testing.T) {
    +	tvc := NewTxVersionChecker(1)
    +
    +	require.NotPanics(t, func() {
    +		err := tvc.CheckTxVersion(nil)
    +		require.Equal(t, process.ErrInvalidTransactionVersion, err)
    +	})
    +}
    +
    +func TestTxVersionChecker_CheckTxVersionBelowMinShouldErr(t *testing.T) {
    +	minTxVersion := uint32(2)
    +	tx := &transaction.Transaction{
    +		RawData: &transaction.Transaction_Raw{
    +			Version: minTxVersion - 1,
    +		},
    +	}
    +	tvc := NewTxVersionChecker(minTxVersion)
    +	err := tvc.CheckTxVersion(tx)
    +	require.Equal(t, process.ErrInvalidTransactionVersion, err)
    +}
    
  • network/p2p/libp2p/netMessenger.go+30 1 modified
    @@ -7,6 +7,7 @@ import (
     	"io"
     	"math"
     	"math/big"
    +	"runtime/debug"
     	"sort"
     	"strings"
     	"sync"
    @@ -888,8 +889,36 @@ func (netMes *networkMessenger) RegisterMessageProcessor(topic string, handler p
     }
     
     func (netMes *networkMessenger) pubsubCallback(handler p2p.MessageProcessor, topic string) func(ctx context.Context, pid peer.ID, message *pubsub.Message) bool {
    -	return func(ctx context.Context, pid peer.ID, message *pubsub.Message) bool {
    +	return func(ctx context.Context, pid peer.ID, message *pubsub.Message) (isValid bool) {
     		fromConnectedPeer := core.PeerID(pid)
    +
    +		// Circuit-breaker: this callback runs in an unrecovered pubsub goroutine, so a
    +		// panic on an untrusted message would crash the node (GHSA-rm5c-5x2p-48wr).
    +		defer func() {
    +			if r := recover(); r != nil {
    +				// Read message via nil-safe getters: the recover must not panic itself.
    +				var dataLen int
    +				var originator core.PeerID
    +				if message != nil {
    +					dataLen = len(message.GetData())
    +					originator = core.PeerID(message.GetFrom())
    +				}
    +
    +				log.Error("recovered from panic in pubsubCallback",
    +					"topic", topic,
    +					"originator", p2p.PeerIDToShortString(originator),
    +					"from connected peer", p2p.PeerIDToShortString(fromConnectedPeer),
    +					"panic", fmt.Sprintf("%v", r),
    +					"stack", string(debug.Stack()),
    +				)
    +				// Blacklist both peers, as transformAndCheckMessage does on severe errors.
    +				netMes.blacklistPid(fromConnectedPeer, core.WrongP2PMessageBlacklistDuration)
    +				netMes.blacklistPid(originator, core.WrongP2PMessageBlacklistDuration)
    +				netMes.processDebugMessage(topic, fromConnectedPeer, uint64(dataLen), true)
    +				isValid = false
    +			}
    +		}()
    +
     		msg, err := netMes.transformAndCheckMessage(message, fromConnectedPeer, topic)
     		if err != nil {
     			log.Trace("p2p validator - new message", "error", err.Error(), "topic", topic)
    
  • network/p2p/libp2p/netMessenger_test.go+91 0 modified
    @@ -1508,6 +1508,97 @@ func TestNetworkMessenger_PubsubCallbackReturnsFalseIfHandlerErrors(t *testing.T
     	_ = mes.Close()
     }
     
    +// Regression test for GHSA-rm5c-5x2p-48wr: a panic while processing a gossip message
    +// must be recovered (no node crash), reject the message, and blacklist both peers.
    +func TestNetworkMessenger_PubsubCallbackRecoversFromHandlerPanic(t *testing.T) {
    +	args := libp2p.ArgsNetworkMessenger{
    +		Marshalizer:   &commonMock.ProtoMarshalizerMock{},
    +		ListenAddress: libp2p.ListenLocalhostAddrWithIp4AndTcp,
    +		P2pConfig: config.P2PConfig{
    +			Node: config.NodeConfig{
    +				Port: "0",
    +			},
    +			KadDhtPeerDiscovery: config.KadDhtPeerDiscoveryConfig{
    +				Enabled: false,
    +			},
    +			Sharding: config.ShardingConfig{
    +				Type: p2p.NilListSharder,
    +			},
    +		},
    +		SyncTimer: &libp2p.LocalSyncTimer{},
    +	}
    +
    +	mes, _ := libp2p.NewNetworkMessenger(args)
    +
    +	// Distinct ids so we can assert BOTH get blacklisted. The originator must be a
    +	// valid peer id so the message reaches the panicking handler.
    +	connectedPeer := peer.ID("connected-peer-id")
    +	originator := []byte(mes.ID())
    +
    +	var mutBlacklisted sync.Mutex
    +	blacklisted := make(map[string]struct{})
    +	_ = mes.SetPeerDenialEvaluator(&mock.PeerDenialEvaluatorStub{
    +		UpsertPeerIDCalled: func(pid core.PeerID, duration time.Duration) error {
    +			mutBlacklisted.Lock()
    +			blacklisted[string(pid)] = struct{}{}
    +			mutBlacklisted.Unlock()
    +			return nil
    +		},
    +		IsDeniedCalled: func(pid core.PeerID) bool {
    +			return false
    +		},
    +	})
    +
    +	numCalled := uint32(0)
    +	handler := &mock.MessageProcessorStub{
    +		ProcessMessageCalled: func(message p2p.MessageP2P, fromConnectedPeer core.PeerID) error {
    +			atomic.AddUint32(&numCalled, 1)
    +			panic("simulated nil-pointer dereference while processing a malicious tx")
    +		},
    +	}
    +
    +	callBackFunc := mes.PubsubCallback(handler, "")
    +	ctx := context.Background()
    +	innerMessage := &data.TopicMessage{
    +		Payload:   []byte("data"),
    +		Timestamp: time.Now().Unix(),
    +		Version:   libp2p.CurrentTopicMessageVersion,
    +	}
    +	buff, _ := args.Marshalizer.Marshal(innerMessage)
    +	topic := "topic"
    +	msg := &pubsub.Message{
    +		Message: &pubsub_pb.Message{
    +			From:                 originator,
    +			Data:                 buff,
    +			Seqno:                []byte{0, 0, 0, 1},
    +			Topic:                &topic,
    +			Signature:            nil,
    +			Key:                  nil,
    +			XXX_NoUnkeyedLiteral: struct{}{},
    +			XXX_unrecognized:     nil,
    +			XXX_sizecache:        0,
    +		},
    +		ReceivedFrom:  "",
    +		ValidatorData: nil,
    +	}
    +
    +	isValid := true
    +	require.NotPanics(t, func() {
    +		isValid = callBackFunc(ctx, connectedPeer, msg)
    +	})
    +	assert.False(t, isValid)
    +	assert.Equal(t, uint32(1), atomic.LoadUint32(&numCalled))
    +
    +	mutBlacklisted.Lock()
    +	_, connectedBlacklisted := blacklisted[string(connectedPeer)]
    +	_, originatorBlacklisted := blacklisted[string(originator)]
    +	mutBlacklisted.Unlock()
    +	assert.True(t, connectedBlacklisted, "connected peer should be blacklisted on panic")
    +	assert.True(t, originatorBlacklisted, "originator should be blacklisted on panic")
    +
    +	_ = mes.Close()
    +}
    +
     func TestNetworkMessenger_UnjoinAllTopicsShouldWork(t *testing.T) {
     	args := libp2p.ArgsNetworkMessenger{
     		Marshalizer:   &commonMock.ProtoMarshalizerMock{},
    
25ba0ba4ae70

Merge commit from fork

https://github.com/klever-io/klever-goFernando SobreiraMay 29, 2026Fixed in 1.7.18via ghsa-release-walk
20 files changed · +1031 61
  • common/errors.go+12 0 modified
    @@ -784,6 +784,18 @@ var ErrDecompressionTooLarge = errors.New("decompressed payload exceeds maximum
     // ErrDecompressedSizeMismatch signals that the inflated payload size does not match the advertised DataSize
     var ErrDecompressedSizeMismatch = errors.New("decompressed payload size does not match advertised data size")
     
    +// ErrTooManyItemsInBatch signals that a received Batch carries more items than allowed
    +var ErrTooManyItemsInBatch = errors.New("too many items in batch")
    +
    +// ErrBatchWireTooLarge signals that an incoming Batch wire payload exceeds the
    +// pre-Unmarshal byte cap (MaxBatchWireSize / MaxHashArrayBuffSize).
    +// Distinct from ErrTooManyItemsInBatch so logs / metrics can tell a byte-size
    +// rejection apart from an entry-count rejection.
    +var ErrBatchWireTooLarge = errors.New("batch wire payload too large")
    +
    +// ErrProcessReceivedMessagePanicked signals that ProcessReceivedMessage recovered from a panic
    +var ErrProcessReceivedMessagePanicked = errors.New("process received message panicked")
    +
     // ErrInvalidParameter signals that a wrong parameter has been provided
     var ErrInvalidParameter = errors.New("invalid parameter")
     
    
  • common/mock/throttlerStub.go+16 0 modified
    @@ -1,12 +1,16 @@
     package mock
     
    +import "sync/atomic"
    +
     // ThrottlerStub -
     type ThrottlerStub struct {
     	CanProcessCalled      func() bool
     	StartProcessingCalled func()
     	EndProcessingCalled   func()
     	StartWasCalled        bool
     	EndWasCalled          bool
    +	startProcessingCount  int32
    +	endProcessingCount    int32
     }
     
     // CanProcess -
    @@ -21,6 +25,7 @@ func (ts *ThrottlerStub) CanProcess() bool {
     // StartProcessing -
     func (ts *ThrottlerStub) StartProcessing() {
     	ts.StartWasCalled = true
    +	atomic.AddInt32(&ts.startProcessingCount, 1)
     	if ts.StartProcessingCalled != nil {
     		ts.StartProcessingCalled()
     	}
    @@ -29,11 +34,22 @@ func (ts *ThrottlerStub) StartProcessing() {
     // EndProcessing -
     func (ts *ThrottlerStub) EndProcessing() {
     	ts.EndWasCalled = true
    +	atomic.AddInt32(&ts.endProcessingCount, 1)
     	if ts.EndProcessingCalled != nil {
     		ts.EndProcessingCalled()
     	}
     }
     
    +// StartProcessingCount returns the number of StartProcessing invocations.
    +func (ts *ThrottlerStub) StartProcessingCount() int32 {
    +	return atomic.LoadInt32(&ts.startProcessingCount)
    +}
    +
    +// EndProcessingCount returns the number of EndProcessing invocations.
    +func (ts *ThrottlerStub) EndProcessingCount() int32 {
    +	return atomic.LoadInt32(&ts.endProcessingCount)
    +}
    +
     // IsInterfaceNil -
     func (ts *ThrottlerStub) IsInterfaceNil() bool {
     	return ts == nil
    
  • core/partitioning/simpleDataPacker.go+5 2 modified
    @@ -57,9 +57,12 @@ func (sdp *SimpleDataPacker) PackDataInChunks(data [][]byte, limit int) ([][]byt
     			ba := &batch.Batch{Data: currentChunk}
     			err := ba.Compress(sdp.marshalizer)
     			if err != nil {
    -				continue
    +				return nil, err
    +			}
    +			marshaledChunk, err := sdp.marshalizer.Marshal(ba)
    +			if err != nil {
    +				return nil, err
     			}
    -			marshaledChunk, _ := sdp.marshalizer.Marshal(ba)
     			compressedSize += len(marshaledChunk)
     			returningBuff = append(returningBuff, marshaledChunk)
     			currentChunk = make([][]byte, 0)
    
  • core/partitioning/simpleDataPacker_test.go+33 0 modified
    @@ -2,12 +2,14 @@ package partitioning_test
     
     import (
     	"crypto/rand"
    +	"errors"
     	"testing"
     
     	"github.com/klever-io/klever-go/common"
     	"github.com/klever-io/klever-go/common/mock"
     	"github.com/klever-io/klever-go/core/partitioning"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     )
     
     func TestNewSimpleDataPacker_NilMarshalizerShouldErr(t *testing.T) {
    @@ -116,3 +118,34 @@ func TestSimpleSplitter_SendDataInChunksWithOnlyOneLargeElementShouldWork(t *tes
     	assert.Equal(t, 1, len(buffSent))
     	assert.Nil(t, checkExpectedElements(buffSent[0], marshalizer, [][]byte{elemLarge}))
     }
    +
    +// Regression: a Marshal error inside the chunk-flush branch must propagate to
    +// the caller, not silently drop the chunk and the current element.
    +func TestSimpleDataPacker_PackDataInChunks_PropagatesMarshalError(t *testing.T) {
    +	t.Parallel()
    +
    +	expectedErr := errors.New("forced marshal error")
    +	// Fail only on Batch marshal (the Compress path internally marshals the
    +	// Batch). The trailing-block already returns errors; this exercises the
    +	// in-loop flush branch.
    +	marshalizer := &mock.MarshalizerStub{
    +		MarshalCalled: func(obj interface{}) ([]byte, error) {
    +			return nil, expectedErr
    +		},
    +		UnmarshalCalled: func(obj interface{}, buff []byte) error {
    +			return nil
    +		},
    +	}
    +
    +	sdp, err := partitioning.NewSimpleDataPacker(marshalizer)
    +	require.NoError(t, err)
    +
    +	// Two elements summing to >= limit so the in-loop flush branch is entered.
    +	elem1 := make([]byte, 600)
    +	elem2 := make([]byte, 600)
    +	buffSent, err := sdp.PackDataInChunks([][]byte{elem1, elem2}, 1000)
    +
    +	require.Error(t, err)
    +	require.ErrorIs(t, err, expectedErr)
    +	assert.Nil(t, buffSent)
    +}
    
  • core/process/errors.go+12 4 modified
    @@ -1,6 +1,10 @@
     package process
     
    -import "errors"
    +import (
    +	"errors"
    +
    +	"github.com/klever-io/klever-go/common"
    +)
     
     // ErrNilQuotaStatusHandler signals that a nil quota status handler has been provided
     var ErrNilQuotaStatusHandler = errors.New("nil quota status handler")
    @@ -56,9 +60,13 @@ var ErrEmptyPeerID = errors.New("empty peer ID")
     // ErrNoDataInMessage signals that no data was found after parsing received p2p message
     var ErrNoDataInMessage = errors.New("no data found in received message")
     
    -// ErrTooManyItemsInBatch signals that a received Batch carries more items than allowed,
    -// guarding against pre-allocation amplification (CWE-789 / CWE-770).
    -var ErrTooManyItemsInBatch = errors.New("too many items in batch")
    +// ErrTooManyItemsInBatch aliases common.ErrTooManyItemsInBatch so every Batch consumer
    +// shares a single sentinel under errors.Is.
    +var ErrTooManyItemsInBatch = common.ErrTooManyItemsInBatch
    +
    +// ErrBatchWireTooLarge aliases common.ErrBatchWireTooLarge for the byte-size
    +// pre-Unmarshal rejection path; see common.ErrBatchWireTooLarge.
    +var ErrBatchWireTooLarge = common.ErrBatchWireTooLarge
     
     // ErrInterceptedDataNotForCurrentShard signals that intercepted data is not for current shard
     var ErrInterceptedDataNotForCurrentShard = errors.New("intercepted data not for current shard")
    
  • core/process/interceptors/multiDataInterceptor.go+9 17 modified
    @@ -12,16 +12,9 @@ import (
     	"github.com/klever-io/klever-go/tools/marshal"
     )
     
    -// MaxItemsPerBatch is the hard upper bound on the number of items a single P2P Batch
    -// may carry. It guards ProcessReceivedMessage's pre-allocation
    -// (make([]process.InterceptedData, len(b.Data))) against attacker-controlled lengths
    -// that would otherwise force ~16 B per entry of allocation before any anti-flood check.
    -// See GHSA-74m6-4hjp-7226 / KLC-2353 (CWE-789 / CWE-770).
    -//
    -// Sized at 8192 — comfortably above the ~1700 minimum-sized txs that fit inside
    -// core.MaxBulkTransactionSize (256 KiB), and above any legitimate trie-node response
    -// bounded by core.MaxBufferSizeToSendTrieNodes (also 256 KiB).
    -const MaxItemsPerBatch = 8192
    +// MaxItemsPerBatch aliases batch.MaxItemsPerBatch so every Batch consumer shares a
    +// single cap. See GHSA-74m6-4hjp-7226 / KLC-2353 and GHSA-w342-mj6g-v9c4.
    +const MaxItemsPerBatch = batch.MaxItemsPerBatch
     
     // ArgMultiDataInterceptor is the argument for the multi-data interceptor
     type ArgMultiDataInterceptor struct {
    @@ -105,13 +98,12 @@ func (mdi *MultiDataInterceptor) ProcessReceivedMessage(message p2p.MessageP2P,
     		}
     	}()
     
    -	// We deliberately do NOT add a wire-size pre-check before Unmarshal:
    -	// ProcessReceivedMessage is only invoked from networkMessenger.pubsubCallback
    -	// (network/p2p/libp2p/netMessenger.go), so the libp2p pubsub
    -	// DefaultMaxMessageSize (1 MiB) is always upstream of us. A duplicate cap at
    -	// our layer would be dead code today. If pubsub.WithMaxMessageSize is ever
    -	// raised, reintroduce a MaxRawBatchSize check here. See GHSA-74m6-4hjp-7226 /
    -	// KLC-2353.
    +	// Reject oversized wire payloads before Unmarshal; see MaxBatchWireSize doc.
    +	// No blacklist (logical bound, antiflood gates abuse). Currently unreachable
    +	// under default libp2p; forward-defense for raised pubsub.WithMaxMessageSize.
    +	if len(message.Data()) > batch.MaxBatchWireSize {
    +		return process.ErrBatchWireTooLarge
    +	}
     	b := batch.Batch{}
     	err = mdi.marshalizer.Unmarshal(&b, message.Data())
     	if err != nil {
    
  • core/process/interceptors/multiDataInterceptor_test.go+74 0 modified
    @@ -17,6 +17,7 @@ import (
     	"github.com/klever-io/klever-go/core/throttler"
     	"github.com/klever-io/klever-go/data/batch"
     	"github.com/klever-io/klever-go/tools/check"
    +	"github.com/klever-io/klever-go/tools/marshal/factory"
     	"github.com/stretchr/testify/assert"
     	"github.com/stretchr/testify/require"
     )
    @@ -822,6 +823,79 @@ func TestMultiDataInterceptor_ProcessReceivedMessage_CompressedItemCountBomb_Rel
     		"the post-Decompress items-per-batch rejection path must release the throttler slot")
     }
     
    +// Regression GHSA-w342-mj6g-v9c4 defense-in-depth: oversized wire payload must
    +// be rejected before Unmarshal (slice-header amplification window).
    +func TestMultiDataInterceptor_ProcessReceivedMessage_RejectsOversizedWirePayload(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	// Junk bytes (not a marshaled Batch): on vulnerable code Unmarshal would
    +	// either error or decode to empty; only the fix returns ErrTooManyItemsInBatch.
    +	payload := make([]byte, batch.MaxBatchWireSize+1)
    +
    +	countingThrottler := &mock.InterceptorThrottlerStub{
    +		CanProcessCalled: func() bool { return true },
    +	}
    +
    +	arg := createMockArgMultiDataInterceptor()
    +	arg.Marshalizer = marshalizer
    +	arg.Throttler = countingThrottler
    +
    +	mdi, err := interceptors.NewMultiDataInterceptor(arg)
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{
    +		DataField:  payload,
    +		PeerField:  core.PeerID("origin-peer"),
    +		SeqNoField: []byte("seq-1"),
    +	}
    +
    +	processErr := mdi.ProcessReceivedMessage(msg, fromConnectedPeerID)
    +	require.ErrorIs(t, processErr, process.ErrBatchWireTooLarge)
    +	assert.Equal(t, int32(1), countingThrottler.StartProcessingCount())
    +	assert.Equal(t, int32(1), countingThrottler.EndProcessingCount(),
    +		"the wire-size rejection path must release the throttler slot")
    +}
    +
    +// Regression GHSA-w342-mj6g-v9c4: real amplification pattern. Payload is `0x0a 0x00` × N —
    +// proto3 field-1 LEN tag + length-0 varint = one empty `repeated bytes` entry per pair.
    +// Pre-check must reject on byte length before Unmarshal allocates N empty []byte headers.
    +func TestMultiDataInterceptor_ProcessReceivedMessage_RejectsRealAmplificationPattern(t *testing.T) {
    +	t.Parallel()
    +
    +	// Proto marshalizer so the pattern decodes (not just a size rejection) if pre-check is bypassed.
    +	protoMarsh, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    +	require.NoError(t, err)
    +
    +	const numEmptyEntries = batch.MaxBatchWireSize/2 + 1
    +	payload := bytes.Repeat([]byte{0x0a, 0x00}, numEmptyEntries)
    +
    +	countingThrottler := &mock.InterceptorThrottlerStub{
    +		CanProcessCalled: func() bool { return true },
    +	}
    +
    +	arg := createMockArgMultiDataInterceptor()
    +	arg.Marshalizer = protoMarsh
    +	arg.Throttler = countingThrottler
    +
    +	mdi, err := interceptors.NewMultiDataInterceptor(arg)
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{
    +		DataField:  payload,
    +		PeerField:  core.PeerID("origin-peer"),
    +		SeqNoField: []byte("seq-1"),
    +	}
    +
    +	processErr := mdi.ProcessReceivedMessage(msg, fromConnectedPeerID)
    +	require.ErrorIs(t, processErr, process.ErrBatchWireTooLarge,
    +		"wire-size pre-check must fire before proto Unmarshal allocates %d empty entries", numEmptyEntries)
    +	assert.Equal(t, int32(1), countingThrottler.StartProcessingCount())
    +	assert.Equal(t, int32(1), countingThrottler.EndProcessingCount(),
    +		"the wire-size rejection path must release the throttler slot")
    +}
    +
     //------- regression: data race on bdi.debugHandler from worker goroutine (Finding 4.2)
     
     // MultiDataInterceptor.ProcessReceivedMessage spawns a worker goroutine that
    
  • data/batch/batch.go+31 9 modified
    @@ -12,15 +12,33 @@ import (
     	"github.com/klever-io/klever-go/tools/marshal"
     )
     
    -// MaxDecompressedBatchSize is the hard upper bound on the inflated size of a Batch payload.
    -// It guards against gzip "decompression bomb" attacks where a tiny wire payload expands to
    -// many gigabytes inside io.ReadAll. See GHSA-74m6-4hjp-7226 / KLC-2352 (CWE-409).
    -//
    -// Sized at 10 MiB — ~40x the legitimate single-batch ceiling enforced upstream
    -// (core.MaxBulkTransactionSize and core.MaxBufferSizeToSendTrieNodes are both 256 KiB),
    -// equal to one second of the outOfSpecs per-peer antiflood budget, and well below the
    -// 30-second blacklist threshold (~36 MiB).
    -const MaxDecompressedBatchSize = 10 * 1024 * 1024
    +// MaxBatchWireSize caps the COMPRESSED wire payload at the multi-data interceptor
    +// pre-Unmarshal site. Equal to libp2p's DefaultMaxMessageSize; coordinated with
    +// the tighter outbound network/p2p/libp2p.maxSendBuffSize (1 MiB − 64 KiB framing).
    +// Does NOT bound Decompress output — see MaxDecompressedBatchSize for that.
    +// See GHSA-74m6-4hjp-7226 / KLC-2352 (CWE-409), GHSA-w342-mj6g-v9c4.
    +const MaxBatchWireSize = 1 << 20 // 1 MiB
    +
    +// MaxDecompressedBatchSize caps the INFLATED output of Batch.Decompress.
    +// Sized at 2 MiB — ~2× the worst-case legitimate marshaled batch, which is a
    +// single oversized-element chunk carrying one tx near core.MaxDataSize (1 MiB)
    +// plus tx + Batch proto framing. Distinct from MaxBatchWireSize because a
    +// highly-compressible payload below the wire cap can legitimately inflate
    +// past 1 MiB on Decompress. Bounds transient slice-header amplification to
    +// ~24 MB before MaxItemsPerBatch fires.
    +// See GHSA-74m6-4hjp-7226 / KLC-2352 (CWE-409), GHSA-w342-mj6g-v9c4.
    +const MaxDecompressedBatchSize = 1 << 21 // 2 MiB
    +
    +// MaxItemsPerBatch caps Batch entries. A byte cap alone isn't enough: 2 MiB of
    +// mostly-empty proto entries still encodes ~1 M items.
    +// See GHSA-w342-mj6g-v9c4 and GHSA-74m6-4hjp-7226 / KLC-2353.
    +const MaxItemsPerBatch = 8192
    +
    +// MaxHashArrayBuffSize is the tighter pre-Unmarshal cap for resolver hash-array
    +// requests. Sized at 512 KiB — ~1.9× the security ceiling
    +// (MaxItemsPerBatch × ~34 B proto framing for 32-byte hash entries ≈ 272 KiB).
    +// Defense-in-depth for GHSA-w342-mj6g-v9c4.
    +const MaxHashArrayBuffSize = 1 << 19 // 512 KiB
     
     // New returns a new batch from given buffers
     func New(buffs ...[]byte) *Batch {
    @@ -154,6 +172,10 @@ func (ba *Batch) Decompress(m marshal.Marshalizer) error {
     		return err
     	}
     
    +	if len(ba.Data) > MaxItemsPerBatch {
    +		return common.ErrTooManyItemsInBatch
    +	}
    +
     	ba.Stream = nil
     	ba.IsCompressed = false
     	return nil
    
  • data/batch/batch_test.go+112 0 modified
    @@ -180,6 +180,118 @@ func TestDecompress_RejectsDataSizeMismatch(t *testing.T) {
     	}
     }
     
    +// Regression: GHSA-w342-mj6g-v9c4 — inflated entry count over MaxItemsPerBatch.
    +func TestDecompress_RejectsItemCountBomb(t *testing.T) {
    +	t.Parallel()
    +
    +	internalMarshalizer, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    +	require.NoError(t, err)
    +
    +	bomb := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: make([][]byte, batch.MaxItemsPerBatch+1),
    +	}
    +	require.NoError(t, bomb.Compress(internalMarshalizer))
    +
    +	tampered := &batch.Batch{
    +		IsCompressed: true,
    +		Algo:         bomb.Algo,
    +		Stream:       bomb.Stream,
    +		DataSize:     bomb.DataSize,
    +	}
    +
    +	err = tampered.Decompress(internalMarshalizer)
    +	require.Error(t, err)
    +	require.Truef(t, errors.Is(err, common.ErrTooManyItemsInBatch),
    +		"expected ErrTooManyItemsInBatch, got %v", err)
    +}
    +
    +// Pins the cap boundary against off-by-one regressions.
    +func TestDecompress_AcceptsItemCountAtCap(t *testing.T) {
    +	t.Parallel()
    +
    +	internalMarshalizer, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    +	require.NoError(t, err)
    +
    +	ok := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: make([][]byte, batch.MaxItemsPerBatch),
    +	}
    +	require.NoError(t, ok.Compress(internalMarshalizer))
    +	require.NoError(t, ok.Decompress(internalMarshalizer))
    +	require.Equal(t, batch.MaxItemsPerBatch, len(ok.Data))
    +}
    +
    +// Pins the Decompress cap boundary against off-by-one regressions.
    +// Builds a Batch whose marshaled-pre-compression size equals exactly
    +// MaxDecompressedBatchSize — one repeated-bytes entry of size N has framing
    +// of tag(1B) + length-varint(varies) + N. For N up to 2^21 − 1 the length
    +// varint is 3 B, so entrySize = MaxDecompressedBatchSize − 4.
    +func TestDecompress_AcceptsAtDecompressedBoundary(t *testing.T) {
    +	t.Parallel()
    +
    +	internalMarshalizer, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    +	require.NoError(t, err)
    +
    +	const entrySize = batch.MaxDecompressedBatchSize - 4
    +
    +	b := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: [][]byte{make([]byte, entrySize)},
    +	}
    +	require.NoError(t, b.Compress(internalMarshalizer))
    +	require.Equal(t, int32(batch.MaxDecompressedBatchSize), b.DataSize, // #nosec G115 — fits int32 by construction
    +		"sanity: marshaled-pre-compression size must equal MaxDecompressedBatchSize")
    +
    +	incoming := &batch.Batch{
    +		IsCompressed: true,
    +		Algo:         b.Algo,
    +		Stream:       b.Stream,
    +		DataSize:     b.DataSize,
    +	}
    +	require.NoError(t, incoming.Decompress(internalMarshalizer),
    +		"Decompress must accept an inflated payload of exactly MaxDecompressedBatchSize")
    +	require.Equal(t, 1, len(incoming.Data))
    +	require.Equal(t, entrySize, len(incoming.Data[0]))
    +}
    +
    +// Regression for the F1 finding: a max-cap single-tx batch's marshaled size
    +// EXCEEDS MaxBatchWireSize by the size of proto framing. That payload must
    +// still round-trip cleanly through Compress → Decompress — which is the whole
    +// reason MaxDecompressedBatchSize is sized above MaxBatchWireSize. If
    +// MaxDecompressedBatchSize ever drops to MaxBatchWireSize, this test fails.
    +func TestDecompress_AcceptsMaxCapSingleTxBatch(t *testing.T) {
    +	t.Parallel()
    +
    +	internalMarshalizer, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    +	require.NoError(t, err)
    +
    +	// One entry sized at core.MaxDataSize (1 MiB) — proxy for a max-cap tx.
    +	// Marshaled batch is 1 + 3 + (1 << 20) = 1 MiB + 4 bytes, i.e. just above
    +	// MaxBatchWireSize. The all-zero content is realistic for SC-deploy
    +	// bytecode with padding and is the case that triggers the F1 gap.
    +	const txInner = 1 << 20
    +
    +	b := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: [][]byte{make([]byte, txInner)},
    +	}
    +	require.NoError(t, b.Compress(internalMarshalizer))
    +	require.Greater(t, b.DataSize, int32(batch.MaxBatchWireSize), // #nosec G115 — fits int32 by construction
    +		"sanity: a max-cap single-tx batch's marshaled size DOES exceed MaxBatchWireSize — that's the framing gap MaxDecompressedBatchSize accommodates")
    +
    +	incoming := &batch.Batch{
    +		IsCompressed: true,
    +		Algo:         b.Algo,
    +		Stream:       b.Stream,
    +		DataSize:     b.DataSize,
    +	}
    +	require.NoError(t, incoming.Decompress(internalMarshalizer),
    +		"Decompress must accept a max-cap single-tx batch (DataSize > MaxBatchWireSize but ≤ MaxDecompressedBatchSize)")
    +	require.Equal(t, 1, len(incoming.Data))
    +	require.Equal(t, txInner, len(incoming.Data[0]))
    +}
    +
     func BenchmarkCompress(b *testing.B) {
     	algos := []batch.CType{batch.CType_GZip, batch.CType_LZ4}
     	sizes := []int{100, 1000, 3000}
    
  • data/retriever/resolvers/headerResolver.go+17 2 modified
    @@ -1,6 +1,9 @@
     package resolvers
     
     import (
    +	"fmt"
    +	"runtime/debug"
    +
     	logger "github.com/klever-io/klever-go-logger"
     	"github.com/klever-io/klever-go/common"
     	"github.com/klever-io/klever-go/core"
    @@ -106,8 +109,17 @@ func (hdrRes *HeaderResolver) SetEpochHandler(epochHandler retriever.EpochHandle
     
     // ProcessReceivedMessage will be the callback func from the p2p.Messenger and will be called each time a new message was received
     // (for the topic this validator was registered to, usually a request topic)
    -func (hdrRes *HeaderResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) error {
    -	err := hdrRes.canProcessMessage(message, fromConnectedPeer)
    +func (hdrRes *HeaderResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) (err error) {
    +	defer func() {
    +		if r := recover(); r != nil {
    +			log.Error("HeaderResolver.ProcessReceivedMessage panicked",
    +				"panic", r,
    +				"stack", string(debug.Stack()))
    +			err = fmt.Errorf("%w: %v", common.ErrProcessReceivedMessagePanicked, r)
    +		}
    +	}()
    +
    +	err = hdrRes.canProcessMessage(message, fromConnectedPeer)
     	if err != nil {
     		return err
     	}
    @@ -194,6 +206,9 @@ func (hdrRes *HeaderResolver) searchInCache(nonce uint64) ([]byte, error) {
     	if err != nil {
     		return nil, err
     	}
    +	if len(headers) == 0 {
    +		return nil, common.ErrMissingData
    +	}
     
     	hdr := headers[len(headers)-1]
     	buff, err := hdrRes.marshalizer.Marshal(hdr)
    
  • data/retriever/resolvers/headerResolver_test.go+58 0 modified
    @@ -695,3 +695,61 @@ func TestHeaderResolver_SetAndGetNumPeersToQuery(t *testing.T) {
     	assert.Equal(t, expectedIntra, actualIntra)
     	assert.Equal(t, expectedCross, actualCross)
     }
    +
    +// Regression: searchInCache must not panic when the headers pool returns
    +// (emptySlice, _, nil). HeadersCacherStub default returns exactly that.
    +func TestHeaderResolver_ProcessReceivedMessage_NonceType_EmptyHeadersNoPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createMockArgHeaderResolver()
    +	// Force the storage lookup to fail so the code falls through to searchInCache.
    +	arg.HeadersNoncesStorage = &mock.StorerStub{
    +		SearchFirstCalled: func(key []byte) ([]byte, error) {
    +			return nil, errKeyNotFound
    +		},
    +	}
    +	// arg.Headers (HeadersCacherStub) returns (nil, nil, nil) by default — the
    +	// exact empty-with-nil-err state that previously panicked at headers[len-1].
    +
    +	hdrRes, err := resolvers.NewHeaderResolver(arg)
    +	require.NoError(t, err)
    +
    +	nonceBytes := arg.NonceConverter.ToByteSlice(uint64(42))
    +	processErr := hdrRes.ProcessReceivedMessage(
    +		createRequestMsg(retriever.RequestDataType_NonceType, nonceBytes),
    +		fromConnectedPeerId,
    +	)
    +	require.Error(t, processErr)
    +	require.Truef(t, errors.Is(processErr, common.ErrMissingData),
    +		"expected ErrMissingData, got %v", processErr)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    +
    +// Regression: a panic in any dependency must be recovered by
    +// ProcessReceivedMessage rather than killing the message-handling goroutine.
    +func TestHeaderResolver_ProcessReceivedMessage_RecoversFromPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createMockArgHeaderResolver()
    +	// Inject a marshalizer whose Unmarshal panics — this fires inside
    +	// parseReceivedMessage, after canProcessMessage / throttler.Start.
    +	arg.Marshalizer = &mock.MarshalizerStub{
    +		UnmarshalCalled: func(obj interface{}, buff []byte) error {
    +			panic("injected panic during Unmarshal")
    +		},
    +	}
    +
    +	hdrRes, err := resolvers.NewHeaderResolver(arg)
    +	require.NoError(t, err)
    +
    +	processErr := hdrRes.ProcessReceivedMessage(
    +		createRequestMsg(retriever.RequestDataType_HashType, []byte("x")),
    +		fromConnectedPeerId,
    +	)
    +	require.Error(t, processErr)
    +	require.Truef(t, errors.Is(processErr, common.ErrProcessReceivedMessagePanicked),
    +		"expected ErrProcessReceivedMessagePanicked, got %v", processErr)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    
  • data/retriever/resolvers/topicResolverSender/topicResolverSender.go+6 5 modified
    @@ -105,12 +105,13 @@ func (trs *topicResolverSender) SendOnRequestTopic(rd *retriever.RequestData, or
     	}
     
     	topicToSendRequest := trs.topicName + topicRequestSuffix
    +	numConsensusPeers, numCommonPeers := trs.NumPeersToQuery()
     
     	commonPeers := trs.peerListCreator.PeerList()
    -	numSentCommon := trs.sendOnTopic(commonPeers, topicToSendRequest, buff, trs.numCommonPeers, "common peer")
    +	numSentCommon := trs.sendOnTopic(commonPeers, topicToSendRequest, buff, numCommonPeers, "common peer")
     
     	consensusPeers := trs.peerListCreator.ConsensusPeerList()
    -	numSentConsensus := trs.sendOnTopic(consensusPeers, topicToSendRequest, buff, trs.numConsensusPeers, "consensus peer")
    +	numSentConsensus := trs.sendOnTopic(consensusPeers, topicToSendRequest, buff, numConsensusPeers, "consensus peer")
     
     	trs.callDebugHandler(originalHashes, numSentConsensus, numSentCommon)
     
    @@ -146,9 +147,9 @@ func (trs *topicResolverSender) SendOnRequestTopicTo(rd *retriever.RequestData,
     		}
     	}
     
    -	numConsensusPeers := trs.numConsensusPeers
    -	if numSentDirect > 0 {
    -		// if sent to origin, remove one from max to send
    +	numConsensusPeers, _ := trs.NumPeersToQuery()
    +	if numSentDirect > 0 && numConsensusPeers > 0 {
    +		// origin already got direct send; the > 0 guard prevents fan-out on NumConsensusPeers=0
     		numConsensusPeers = numConsensusPeers - 1
     	}
     	numSentConsensus := trs.sendOnTopic(consensusPeers, topicToSendRequest, buff, numConsensusPeers, "consensus peer")
    
  • data/retriever/resolvers/topicResolverSender/topicResolverSender_test.go+94 0 modified
    @@ -14,6 +14,7 @@ import (
     	"github.com/klever-io/klever-go/network/p2p"
     	"github.com/klever-io/klever-go/tools/check"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     )
     
     var defaultHashes = [][]byte{[]byte("hash")}
    @@ -512,3 +513,96 @@ func TestTopicResolverSender_NumPeersToQueryr(t *testing.T) {
     	assert.Equal(t, intra, recoveredIntra)
     	assert.Equal(t, cross, recoveredCross)
     }
    +
    +// Regression: SendOnRequestTopicTo must not fan out to every consensus peer when
    +// the resolver is configured with NumConsensusPeers=0 (a valid config when
    +// NumCommonPeers>=2). The pre-fix code computed numConsensusPeers-1 = -1 and the
    +// sendOnTopic break (`msgSentCounter == maxToSend`) never fired, broadcasting to
    +// the entire consensus list. With the clamp, the per-call budget stays at 0
    +// (which sendOnTopic floors to 1), so at most one consensus peer is contacted.
    +func TestTopicResolverSender_SendOnRequestTopicTo_ZeroConsensusPeersNoFanout(t *testing.T) {
    +	t.Parallel()
    +
    +	directPeer := core.PeerID("direct")
    +	consensusList := []core.PeerID{
    +		core.PeerID("c1"), core.PeerID("c2"), core.PeerID("c3"),
    +		core.PeerID("c4"), core.PeerID("c5"),
    +	}
    +
    +	consensusSendCount := 0
    +	directSendCount := 0
    +	arg := createMockArgTopicResolverSender()
    +	arg.NumConsensusPeers = 0
    +	arg.NumCommonPeers = 2 // satisfy Common+Consensus >= 2 constructor validation
    +	arg.PeerListCreator = &mock.PeerListCreatorStub{
    +		PeerListCalled:          func() []core.PeerID { return nil },
    +		ConsensusPeerListCalled: func() []core.PeerID { return consensusList },
    +	}
    +	arg.Messenger = &mock.MessageHandlerStub{
    +		SendToConnectedPeerCalled: func(topic string, buff []byte, peerID core.PeerID) error {
    +			if bytes.Equal(peerID.Bytes(), directPeer.Bytes()) {
    +				directSendCount++
    +			} else {
    +				consensusSendCount++
    +			}
    +			return nil
    +		},
    +	}
    +	trs, err := topicResolverSender.NewTopicResolverSender(arg)
    +	require.NoError(t, err)
    +
    +	err = trs.SendOnRequestTopicTo(&retriever.RequestData{}, defaultHashes, directPeer)
    +	require.NoError(t, err)
    +
    +	assert.Equal(t, 1, directSendCount, "direct peer should receive exactly one send")
    +	assert.LessOrEqualf(t, consensusSendCount, 1, "without fan-out the consensus budget caps at 1 (sendOnTopic floors maxToSend=0 to 1); got %d", consensusSendCount)
    +}
    +
    +// Regression: SendOnRequestTopic and SendOnRequestTopicTo must read
    +// numConsensusPeers / numCommonPeers under the same mutex as SetNumPeersToQuery.
    +// Run with `go test -race` to confirm; without the fix the race detector flags
    +// concurrent read/write of numConsensusPeers / numCommonPeers.
    +func TestTopicResolverSender_ConcurrentSendAndSetNumPeers_NoDataRace(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createMockArgTopicResolverSender()
    +	arg.PeerListCreator = &mock.PeerListCreatorStub{
    +		PeerListCalled:          func() []core.PeerID { return []core.PeerID{"p1", "p2", "p3"} },
    +		ConsensusPeerListCalled: func() []core.PeerID { return []core.PeerID{"p4", "p5"} },
    +	}
    +	arg.Messenger = &mock.MessageHandlerStub{
    +		SendToConnectedPeerCalled: func(topic string, buff []byte, peerID core.PeerID) error {
    +			return nil
    +		},
    +	}
    +	trs, err := topicResolverSender.NewTopicResolverSender(arg)
    +	require.NoError(t, err)
    +
    +	rd := &retriever.RequestData{Type: retriever.RequestDataType_HashType, Value: []byte("x")}
    +
    +	const iters = 500
    +	done := make(chan struct{}, 3)
    +
    +	go func() {
    +		for i := 0; i < iters; i++ {
    +			trs.SetNumPeersToQuery(i%5, (i+1)%5)
    +		}
    +		done <- struct{}{}
    +	}()
    +	go func() {
    +		for i := 0; i < iters; i++ {
    +			_ = trs.SendOnRequestTopic(rd, defaultHashes)
    +		}
    +		done <- struct{}{}
    +	}()
    +	go func() {
    +		for i := 0; i < iters; i++ {
    +			_ = trs.SendOnRequestTopicTo(rd, defaultHashes, "peerX")
    +		}
    +		done <- struct{}{}
    +	}()
    +
    +	for i := 0; i < 3; i++ {
    +		<-done
    +	}
    +}
    
  • data/retriever/resolvers/transactionResolver.go+21 2 modified
    @@ -2,6 +2,7 @@ package resolvers
     
     import (
     	"fmt"
    +	"runtime/debug"
     
     	logger "github.com/klever-io/klever-go-logger"
     	"github.com/klever-io/klever-go/common"
    @@ -84,8 +85,17 @@ func NewTxResolver(arg ArgTxResolver) (*TxResolver, error) {
     
     // ProcessReceivedMessage will be the callback func from the p2p.Messenger and will be called each time a new message was received
     // (for the topic this validator was registered to, usually a request topic)
    -func (txRes *TxResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) error {
    -	err := txRes.canProcessMessage(message, fromConnectedPeer)
    +func (txRes *TxResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) (err error) {
    +	defer func() {
    +		if r := recover(); r != nil {
    +			log.Error("TxResolver.ProcessReceivedMessage panicked",
    +				"panic", r,
    +				"stack", string(debug.Stack()))
    +			err = fmt.Errorf("%w: %v", common.ErrProcessReceivedMessagePanicked, r)
    +		}
    +	}()
    +
    +	err = txRes.canProcessMessage(message, fromConnectedPeer)
     	if err != nil {
     		return err
     	}
    @@ -175,11 +185,20 @@ func (txRes *TxResolver) fetchTxAsByteSlice(hash []byte) ([]byte, error) {
     
     func (txRes *TxResolver) resolveTxRequestByHashArray(hashesBuff []byte, pid core.PeerID) error {
     	//TODO this can be optimized by searching in corresponding datapool (taken by topic name)
    +	// Reject oversized hash-array payloads before Unmarshal — see MaxHashArrayBuffSize.
    +	// No blacklist (logical bound, antiflood gates abuse).
    +	if len(hashesBuff) > batch.MaxHashArrayBuffSize {
    +		return common.ErrBatchWireTooLarge
    +	}
     	b := batch.Batch{}
     	err := txRes.marshalizer.Unmarshal(&b, hashesBuff)
     	if err != nil {
     		return err
     	}
    +	// Cap uncompressed batches; the compressed path is bounded inside b.Decompress.
    +	if len(b.Data) > batch.MaxItemsPerBatch {
    +		return common.ErrTooManyItemsInBatch
    +	}
     	if b.IsCompressed {
     		err = b.Decompress(txRes.marshalizer)
     		if err != nil {
    
  • data/retriever/resolvers/transactionResolver_test.go+154 0 modified
    @@ -16,6 +16,7 @@ import (
     	"github.com/klever-io/klever-go/tools/check"
     	"github.com/klever-io/klever-go/tools/marshal"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     )
     
     var connectedPeerId = core.PeerID("connected peer id")
    @@ -552,3 +553,156 @@ func TestTxResolver_SetAndGetNumPeersToQuery(t *testing.T) {
     	assert.Equal(t, expectedIntra, actualIntra)
     	assert.Equal(t, expectedCross, actualCross)
     }
    +
    +//------- regression: GHSA-w342-mj6g-v9c4
    +
    +func TestTxResolver_ProcessReceivedMessage_RejectsHashArrayItemBomb_Uncompressed(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTxResolver()
    +	arg.Marshalizer = marshalizer
    +	txRes, err := resolvers.NewTxResolver(arg)
    +	require.NoError(t, err)
    +
    +	bomb := &batch.Batch{Data: make([][]byte, batch.MaxItemsPerBatch+1)}
    +	buff, err := marshalizer.Marshal(bomb)
    +	require.NoError(t, err)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: buff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := txRes.ProcessReceivedMessage(msg, connectedPeerId)
    +	require.ErrorIs(t, processErr, common.ErrTooManyItemsInBatch)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    +
    +// Regression GHSA-w342-mj6g-v9c4 defense-in-depth: oversized hashesBuff must
    +// be rejected before Unmarshal (slice-header amplification window).
    +func TestTxResolver_ProcessReceivedMessage_RejectsOversizedRawHashArrayBuff(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTxResolver()
    +	arg.Marshalizer = marshalizer
    +	txRes, err := resolvers.NewTxResolver(arg)
    +	require.NoError(t, err)
    +
    +	// Junk bytes (not a marshaled Batch): on vulnerable code Unmarshal would
    +	// either error or decode to empty; only the fix returns ErrTooManyItemsInBatch.
    +	hashesBuff := make([]byte, batch.MaxHashArrayBuffSize+1)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: hashesBuff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := txRes.ProcessReceivedMessage(msg, connectedPeerId)
    +	require.ErrorIs(t, processErr, common.ErrBatchWireTooLarge)
    +	thr := arg.Throttler.(*mock.ThrottlerStub)
    +	assert.Equal(t, int32(1), thr.StartProcessingCount())
    +	assert.Equal(t, int32(1), thr.EndProcessingCount(),
    +		"the wire-size rejection path must release the throttler slot exactly once")
    +}
    +
    +// Regression GHSA-w342-mj6g-v9c4: real amplification pattern. Payload is `0x0a 0x00` × N —
    +// proto3 field-1 LEN tag + length-0 varint = one empty `repeated bytes` entry per pair.
    +// Pre-check must reject on byte length before Unmarshal allocates N empty []byte headers.
    +func TestTxResolver_ProcessReceivedMessage_RejectsRealAmplificationPattern(t *testing.T) {
    +	t.Parallel()
    +
    +	// Proto marshalizer so the pattern decodes (not just a size rejection) if pre-check is bypassed.
    +	protoMarsh := marshal.NewProtoMarshalizer()
    +
    +	arg := createMockArgTxResolver()
    +	arg.Marshalizer = protoMarsh
    +	txRes, err := resolvers.NewTxResolver(arg)
    +	require.NoError(t, err)
    +
    +	const numEmptyEntries = batch.MaxHashArrayBuffSize/2 + 1 // 2 B per entry → just over cap
    +	hashesBuff := bytes.Repeat([]byte{0x0a, 0x00}, numEmptyEntries)
    +
    +	data, err := protoMarsh.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: hashesBuff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := txRes.ProcessReceivedMessage(msg, connectedPeerId)
    +	require.ErrorIs(t, processErr, common.ErrBatchWireTooLarge,
    +		"wire-size pre-check must fire before proto Unmarshal allocates %d empty entries", numEmptyEntries)
    +	thr := arg.Throttler.(*mock.ThrottlerStub)
    +	assert.Equal(t, int32(1), thr.EndProcessingCount(),
    +		"throttler slot must be released on the pre-check rejection path")
    +}
    +
    +func TestTxResolver_ProcessReceivedMessage_RejectsHashArrayItemBomb_Compressed(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTxResolver()
    +	arg.Marshalizer = marshalizer
    +	txRes, err := resolvers.NewTxResolver(arg)
    +	require.NoError(t, err)
    +
    +	bomb := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: make([][]byte, batch.MaxItemsPerBatch+1),
    +	}
    +	require.NoError(t, bomb.Compress(marshalizer))
    +
    +	buff, err := marshalizer.Marshal(bomb)
    +	require.NoError(t, err)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: buff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := txRes.ProcessReceivedMessage(msg, connectedPeerId)
    +	require.ErrorIs(t, processErr, common.ErrTooManyItemsInBatch)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    +
    +// Regression: a panic in any dependency must be recovered by
    +// ProcessReceivedMessage rather than killing the message-handling goroutine.
    +func TestTxResolver_ProcessReceivedMessage_RecoversFromPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createMockArgTxResolver()
    +	arg.Marshalizer = &mock.MarshalizerStub{
    +		UnmarshalCalled: func(obj interface{}, buff []byte) error {
    +			panic("injected panic during Unmarshal")
    +		},
    +	}
    +
    +	txRes, err := resolvers.NewTxResolver(arg)
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: []byte("anything")}
    +
    +	processErr := txRes.ProcessReceivedMessage(msg, connectedPeerId)
    +	require.Error(t, processErr)
    +	require.Truef(t, errors.Is(processErr, common.ErrProcessReceivedMessagePanicked),
    +		"expected ErrProcessReceivedMessagePanicked, got %v", processErr)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    
  • data/retriever/resolvers/trieNodeResolver.go+23 2 modified
    @@ -1,6 +1,9 @@
     package resolvers
     
     import (
    +	"fmt"
    +	"runtime/debug"
    +
     	"github.com/klever-io/klever-go/common"
     	"github.com/klever-io/klever-go/core"
     	"github.com/klever-io/klever-go/data/batch"
    @@ -63,8 +66,17 @@ func NewTrieNodeResolver(arg ArgTrieNodeResolver) (*TrieNodeResolver, error) {
     
     // ProcessReceivedMessage will be the callback func from the p2p.Messenger and will be called each time a new message was received
     // (for the topic this validator was registered to, usually a request topic)
    -func (tnRes *TrieNodeResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) error {
    -	err := tnRes.canProcessMessage(message, fromConnectedPeer)
    +func (tnRes *TrieNodeResolver) ProcessReceivedMessage(message p2p.MessageP2P, fromConnectedPeer core.PeerID) (err error) {
    +	defer func() {
    +		if r := recover(); r != nil {
    +			log.Error("TrieNodeResolver.ProcessReceivedMessage panicked",
    +				"panic", r,
    +				"stack", string(debug.Stack()))
    +			err = fmt.Errorf("%w: %v", common.ErrProcessReceivedMessagePanicked, r)
    +		}
    +	}()
    +
    +	err = tnRes.canProcessMessage(message, fromConnectedPeer)
     	if err != nil {
     		return err
     	}
    @@ -88,11 +100,20 @@ func (tnRes *TrieNodeResolver) ProcessReceivedMessage(message p2p.MessageP2P, fr
     }
     
     func (tnRes *TrieNodeResolver) resolveMultipleHashes(hashesBuff []byte, message p2p.MessageP2P) error {
    +	// Reject oversized hash-array payloads before Unmarshal — see MaxHashArrayBuffSize.
    +	// No blacklist (logical bound, antiflood gates abuse).
    +	if len(hashesBuff) > batch.MaxHashArrayBuffSize {
    +		return common.ErrBatchWireTooLarge
    +	}
     	b := batch.Batch{}
     	err := tnRes.marshalizer.Unmarshal(&b, hashesBuff)
     	if err != nil {
     		return err
     	}
    +	// Cap uncompressed batches; the compressed path is bounded inside b.Decompress.
    +	if len(b.Data) > batch.MaxItemsPerBatch {
    +		return common.ErrTooManyItemsInBatch
    +	}
     	if b.IsCompressed {
     		err = b.Decompress(tnRes.marshalizer)
     		if err != nil {
    
  • data/retriever/resolvers/trieNodeResolver_test.go+156 0 modified
    @@ -8,11 +8,14 @@ import (
     	"github.com/klever-io/klever-go/common"
     	"github.com/klever-io/klever-go/common/mock"
     	"github.com/klever-io/klever-go/core"
    +	"github.com/klever-io/klever-go/data/batch"
     	"github.com/klever-io/klever-go/data/retriever"
     	"github.com/klever-io/klever-go/data/retriever/resolvers"
     	"github.com/klever-io/klever-go/network/p2p"
     	"github.com/klever-io/klever-go/tools/check"
    +	"github.com/klever-io/klever-go/tools/marshal"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     )
     
     var fromConnectedPeer = core.PeerID("from connected peer")
    @@ -297,3 +300,156 @@ func TestTrieNodeResolver_SetAndGetNumPeersToQuery(t *testing.T) {
     	assert.Equal(t, expectedIntra, actualIntra)
     	assert.Equal(t, expectedCross, actualCross)
     }
    +
    +//------- regression: GHSA-w342-mj6g-v9c4
    +
    +func TestTrieNodeResolver_ProcessReceivedMessage_RejectsHashArrayItemBomb_Uncompressed(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTrieNodeResolver()
    +	arg.Marshalizer = marshalizer
    +	tnRes, err := resolvers.NewTrieNodeResolver(arg)
    +	require.NoError(t, err)
    +
    +	bomb := &batch.Batch{Data: make([][]byte, batch.MaxItemsPerBatch+1)}
    +	buff, err := marshalizer.Marshal(bomb)
    +	require.NoError(t, err)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: buff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := tnRes.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	require.ErrorIs(t, processErr, common.ErrTooManyItemsInBatch)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    +
    +// Regression GHSA-w342-mj6g-v9c4 defense-in-depth: oversized hashesBuff must
    +// be rejected before Unmarshal (slice-header amplification window).
    +func TestTrieNodeResolver_ProcessReceivedMessage_RejectsOversizedRawHashArrayBuff(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTrieNodeResolver()
    +	arg.Marshalizer = marshalizer
    +	tnRes, err := resolvers.NewTrieNodeResolver(arg)
    +	require.NoError(t, err)
    +
    +	// Junk bytes (not a marshaled Batch): on vulnerable code Unmarshal would
    +	// either error or decode to empty; only the fix returns ErrTooManyItemsInBatch.
    +	hashesBuff := make([]byte, batch.MaxHashArrayBuffSize+1)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: hashesBuff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := tnRes.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	require.ErrorIs(t, processErr, common.ErrBatchWireTooLarge)
    +	thr := arg.Throttler.(*mock.ThrottlerStub)
    +	assert.Equal(t, int32(1), thr.StartProcessingCount())
    +	assert.Equal(t, int32(1), thr.EndProcessingCount(),
    +		"the wire-size rejection path must release the throttler slot exactly once")
    +}
    +
    +// Regression GHSA-w342-mj6g-v9c4: real amplification pattern. Payload is `0x0a 0x00` × N —
    +// proto3 field-1 LEN tag + length-0 varint = one empty `repeated bytes` entry per pair.
    +// Pre-check must reject on byte length before Unmarshal allocates N empty []byte headers.
    +func TestTrieNodeResolver_ProcessReceivedMessage_RejectsRealAmplificationPattern(t *testing.T) {
    +	t.Parallel()
    +
    +	// Proto marshalizer so the pattern decodes (not just a size rejection) if pre-check is bypassed.
    +	protoMarsh := marshal.NewProtoMarshalizer()
    +
    +	arg := createMockArgTrieNodeResolver()
    +	arg.Marshalizer = protoMarsh
    +	tnRes, err := resolvers.NewTrieNodeResolver(arg)
    +	require.NoError(t, err)
    +
    +	const numEmptyEntries = batch.MaxHashArrayBuffSize/2 + 1
    +	hashesBuff := bytes.Repeat([]byte{0x0a, 0x00}, numEmptyEntries)
    +
    +	data, err := protoMarsh.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: hashesBuff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := tnRes.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	require.ErrorIs(t, processErr, common.ErrBatchWireTooLarge,
    +		"wire-size pre-check must fire before proto Unmarshal allocates %d empty entries", numEmptyEntries)
    +	thr := arg.Throttler.(*mock.ThrottlerStub)
    +	assert.Equal(t, int32(1), thr.EndProcessingCount(),
    +		"throttler slot must be released on the pre-check rejection path")
    +}
    +
    +func TestTrieNodeResolver_ProcessReceivedMessage_RejectsHashArrayItemBomb_Compressed(t *testing.T) {
    +	t.Parallel()
    +
    +	marshalizer := &mock.MarshalizerMock{}
    +
    +	arg := createMockArgTrieNodeResolver()
    +	arg.Marshalizer = marshalizer
    +	tnRes, err := resolvers.NewTrieNodeResolver(arg)
    +	require.NoError(t, err)
    +
    +	bomb := &batch.Batch{
    +		Algo: batch.CType_GZip,
    +		Data: make([][]byte, batch.MaxItemsPerBatch+1),
    +	}
    +	require.NoError(t, bomb.Compress(marshalizer))
    +
    +	buff, err := marshalizer.Marshal(bomb)
    +	require.NoError(t, err)
    +
    +	data, err := marshalizer.Marshal(&retriever.RequestData{
    +		Type:  retriever.RequestDataType_HashArrayType,
    +		Value: buff,
    +	})
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: data}
    +
    +	processErr := tnRes.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	require.ErrorIs(t, processErr, common.ErrTooManyItemsInBatch)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    +
    +// Regression: a panic in any dependency must be recovered by
    +// ProcessReceivedMessage rather than killing the message-handling goroutine.
    +func TestTrieNodeResolver_ProcessReceivedMessage_RecoversFromPanic(t *testing.T) {
    +	t.Parallel()
    +
    +	arg := createMockArgTrieNodeResolver()
    +	arg.Marshalizer = &mock.MarshalizerStub{
    +		UnmarshalCalled: func(obj interface{}, buff []byte) error {
    +			panic("injected panic during Unmarshal")
    +		},
    +	}
    +
    +	tnRes, err := resolvers.NewTrieNodeResolver(arg)
    +	require.NoError(t, err)
    +
    +	msg := &mock.P2PMessageMock{DataField: []byte("anything")}
    +
    +	processErr := tnRes.ProcessReceivedMessage(msg, fromConnectedPeer)
    +	require.Error(t, processErr)
    +	require.Truef(t, errors.Is(processErr, common.ErrProcessReceivedMessagePanicked),
    +		"expected ErrProcessReceivedMessagePanicked, got %v", processErr)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).StartWasCalled)
    +	assert.True(t, arg.Throttler.(*mock.ThrottlerStub).EndWasCalled)
    +}
    
  • data/syncer/kappAccountsSyncer.go+4 9 modified
    @@ -130,11 +130,11 @@ func (k *kappAccountsSyncer) syncAccountDataTries(rootHashes [][]byte, ssh data.
     
     func (k *kappAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisticsHandler, ctx context.Context) error {
     	k.throttler.StartProcessing()
    +	defer k.throttler.EndProcessing()
     
     	k.syncerMutex.Lock()
     	if _, ok := k.dataTries[string(rootHash)]; ok {
     		k.syncerMutex.Unlock()
    -		k.throttler.EndProcessing()
     		return nil
     	}
     
    @@ -160,16 +160,11 @@ func (k *kappAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisti
     		return err
     	}
     	k.trieSyncers[string(rootHash)] = trieSyncer
    +	// Released before the blocking StartSyncing — do NOT move to defer or
    +	// numConcurrentTrieSyncers parallelism collapses.
     	k.syncerMutex.Unlock()
     
    -	err = trieSyncer.StartSyncing(rootHash, ctx)
    -	if err != nil {
    -		return err
    -	}
    -
    -	k.throttler.EndProcessing()
    -
    -	return nil
    +	return trieSyncer.StartSyncing(rootHash, ctx)
     }
     
     func (k *kappAccountsSyncer) findAllAccountRootHashes(mainTrie data.Trie, ctx context.Context) ([][]byte, error) {
    
  • data/syncer/syncDataTrieThrottler_test.go+190 0 added
    @@ -0,0 +1,190 @@
    +package syncer
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	"github.com/klever-io/klever-go/common/mock"
    +	"github.com/klever-io/klever-go/core/throttler"
    +	"github.com/klever-io/klever-go/crypto/hashing/sha256"
    +	"github.com/klever-io/klever-go/data/trie"
    +	"github.com/klever-io/klever-go/data/trie/statistics"
    +	"github.com/klever-io/klever-go/storage"
    +	"github.com/klever-io/klever-go/storage/memorydb"
    +	"github.com/klever-io/klever-go/storage/storageUnit"
    +	"github.com/klever-io/klever-go/tools/marshal"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +// Regression GHSA-fw38-pc54-jvx9 (KLC-2420): every error path out of
    +// syncDataTrie must release the throttler slot. Two timeouts on a throttler
    +// sized to 2 must leave CanProcess()==true; otherwise the parent fan-out
    +// in syncAccountDataTries spins on !CanProcess() forever.
    +
    +func makeSyncerStorageCacher(t *testing.T) (storage.Storer, storage.Cacher) {
    +	t.Helper()
    +
    +	cache, err := storageUnit.NewCache(storageUnit.CacheConfig{
    +		Type:        storageUnit.LRUCache,
    +		Capacity:    16,
    +		Shards:      1,
    +		SizeInBytes: 0,
    +	})
    +	require.NoError(t, err)
    +
    +	persist, err := memorydb.NewlruDB(1024)
    +	require.NoError(t, err)
    +
    +	unit, err := storageUnit.NewStorageUnit(cache, persist)
    +	require.NoError(t, err)
    +
    +	interceptedNodes, err := storageUnit.NewCache(storageUnit.CacheConfig{
    +		Type:        storageUnit.LRUCache,
    +		Capacity:    16,
    +		Shards:      1,
    +		SizeInBytes: 0,
    +	})
    +	require.NoError(t, err)
    +
    +	return unit, interceptedNodes
    +}
    +
    +func newTestUserAccountsSyncer(t *testing.T, max int32) (*userAccountsSyncer, *throttler.NumGoRoutinesThrottler) {
    +	t.Helper()
    +
    +	hasher := &sha256.Sha256{}
    +	marshalizer := marshal.NewProtoMarshalizer()
    +
    +	unit, interceptedNodes := makeSyncerStorageCacher(t)
    +	storageManager, err := trie.NewTrieStorageManagerWithoutPruning(unit)
    +	require.NoError(t, err)
    +
    +	thr, err := throttler.NewNumGoRoutinesThrottler(max)
    +	require.NoError(t, err)
    +
    +	args := ArgsNewUserAccountsSyncer{
    +		ArgsNewBaseAccountsSyncer: ArgsNewBaseAccountsSyncer{
    +			Hasher:                    hasher,
    +			Marshalizer:               marshalizer,
    +			TrieStorageManager:        storageManager,
    +			RequestHandler:            &mock.RequestHandlerStub{},
    +			Timeout:                   time.Second,
    +			Cacher:                    interceptedNodes,
    +			MaxTrieLevelInMemory:      5,
    +			MaxHardCapForMissingNodes: 100,
    +		},
    +		ShardId:   0,
    +		Throttler: thr,
    +	}
    +
    +	syncer, err := NewUserAccountsSyncer(args)
    +	require.NoError(t, err)
    +
    +	return syncer, thr
    +}
    +
    +func newTestKappAccountsSyncer(t *testing.T, max int32) (*kappAccountsSyncer, *throttler.NumGoRoutinesThrottler) {
    +	t.Helper()
    +
    +	hasher := &sha256.Sha256{}
    +	marshalizer := marshal.NewProtoMarshalizer()
    +
    +	unit, interceptedNodes := makeSyncerStorageCacher(t)
    +	storageManager, err := trie.NewTrieStorageManagerWithoutPruning(unit)
    +	require.NoError(t, err)
    +
    +	thr, err := throttler.NewNumGoRoutinesThrottler(max)
    +	require.NoError(t, err)
    +
    +	args := ArgsNewKappAccountsSyncer{
    +		ArgsNewBaseAccountsSyncer: ArgsNewBaseAccountsSyncer{
    +			Hasher:                    hasher,
    +			Marshalizer:               marshalizer,
    +			TrieStorageManager:        storageManager,
    +			RequestHandler:            &mock.RequestHandlerStub{},
    +			Timeout:                   time.Second,
    +			Cacher:                    interceptedNodes,
    +			MaxTrieLevelInMemory:      5,
    +			MaxHardCapForMissingNodes: 100,
    +		},
    +		Throttler: thr,
    +	}
    +
    +	syncer, err := NewKappAccountsSyncer(args)
    +	require.NoError(t, err)
    +
    +	return syncer, thr
    +}
    +
    +func TestUserAccountsSyncer_syncDataTrie_releasesThrottlerOnTimeout(t *testing.T) {
    +	t.Parallel()
    +
    +	syncer, thr := newTestUserAccountsSyncer(t, 2)
    +
    +	ctx := t.Context()
    +
    +	ssh := statistics.NewTrieSyncStatistics()
    +
    +	rootHashes := [][]byte{
    +		[]byte("11111111111111111111111111111111"),
    +		[]byte("22222222222222222222222222222222"),
    +	}
    +
    +	for _, rh := range rootHashes {
    +		err := syncer.syncDataTrie(rh, ssh, ctx)
    +		require.ErrorIs(t, err, trie.ErrTimeIsOut,
    +			"empty storage and no peer responses must surface trie.ErrTimeIsOut")
    +	}
    +
    +	require.True(t, thr.CanProcess(),
    +		"throttler slot must be released on every error path; CanProcess()==false here means the slot leaked (GHSA-fw38-pc54-jvx9)")
    +}
    +
    +func TestKappAccountsSyncer_syncDataTrie_releasesThrottlerOnTimeout(t *testing.T) {
    +	t.Parallel()
    +
    +	syncer, thr := newTestKappAccountsSyncer(t, 2)
    +
    +	ctx := t.Context()
    +
    +	ssh := statistics.NewTrieSyncStatistics()
    +
    +	rootHashes := [][]byte{
    +		[]byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
    +		[]byte("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
    +	}
    +
    +	for _, rh := range rootHashes {
    +		err := syncer.syncDataTrie(rh, ssh, ctx)
    +		require.ErrorIs(t, err, trie.ErrTimeIsOut,
    +			"empty storage and no peer responses must surface trie.ErrTimeIsOut")
    +	}
    +
    +	require.True(t, thr.CanProcess(),
    +		"throttler slot must be released on every error path; CanProcess()==false here means the slot leaked (GHSA-fw38-pc54-jvx9)")
    +}
    +
    +func TestUserAccountsSyncer_syncDataTrie_duplicateRootStillReleasesThrottler(t *testing.T) {
    +	t.Parallel()
    +
    +	syncer, thr := newTestUserAccountsSyncer(t, 2)
    +
    +	ctx := t.Context()
    +
    +	ssh := statistics.NewTrieSyncStatistics()
    +	rootHash := []byte("ccccccccccccccccccccccccccccccccc")
    +
    +	// Pre-seed the dataTries map so the duplicate-root early return is exercised
    +	// without going through trieSyncer.StartSyncing().
    +	syncer.syncerMutex.Lock()
    +	syncer.dataTries[string(rootHash)] = nil
    +	syncer.syncerMutex.Unlock()
    +
    +	for range 3 {
    +		err := syncer.syncDataTrie(rootHash, ssh, ctx)
    +		require.NoError(t, err)
    +	}
    +
    +	require.True(t, thr.CanProcess(),
    +		"duplicate-root early return must release the throttler slot")
    +}
    
  • data/syncer/userAccountsSyncer.go+4 9 modified
    @@ -136,11 +136,11 @@ func (u *userAccountsSyncer) syncAccountDataTries(rootHashes [][]byte, ssh data.
     
     func (u *userAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisticsHandler, ctx context.Context) error {
     	u.throttler.StartProcessing()
    +	defer u.throttler.EndProcessing()
     
     	u.syncerMutex.Lock()
     	if _, ok := u.dataTries[string(rootHash)]; ok {
     		u.syncerMutex.Unlock()
    -		u.throttler.EndProcessing()
     		return nil
     	}
     
    @@ -166,16 +166,11 @@ func (u *userAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisti
     		return err
     	}
     	u.trieSyncers[string(rootHash)] = trieSyncer
    +	// Released before the blocking StartSyncing — do NOT move to defer or
    +	// numConcurrentTrieSyncers parallelism collapses.
     	u.syncerMutex.Unlock()
     
    -	err = trieSyncer.StartSyncing(rootHash, ctx)
    -	if err != nil {
    -		return err
    -	}
    -
    -	u.throttler.EndProcessing()
    -
    -	return nil
    +	return trieSyncer.StartSyncing(rootHash, ctx)
     }
     
     func (u *userAccountsSyncer) findAllAccountRootHashes(mainTrie data.Trie, ctx context.Context) ([][]byte, error) {
    
1ce066b74d84

[KLC-2384] chore(deps): upgrade gopsutil v3 → v4 + bump go-libp2p-kad-dht (clears Dependabot #33) (#50)

https://github.com/klever-io/klever-goFernando SobreiraMay 13, 2026Fixed in 1.7.18via ghsa-release-walk
9 files changed · +77 68
  • core/statistics/machine/common.go+1 1 modified
    @@ -4,7 +4,7 @@ import (
     	"os"
     
     	logger "github.com/klever-io/klever-go-logger"
    -	"github.com/shirou/gopsutil/process"
    +	"github.com/shirou/gopsutil/v4/process"
     )
     
     var log = logger.GetOrCreate("statistics/machine")
    
  • core/statistics/machine/cpuStatistics.go+1 1 modified
    @@ -5,7 +5,7 @@ import (
     	"sync/atomic"
     	"time"
     
    -	"github.com/shirou/gopsutil/cpu"
    +	"github.com/shirou/gopsutil/v4/cpu"
     )
     
     var durationSecond = time.Second
    
  • core/statistics/machine/diskStatistics.go+1 1 modified
    @@ -7,7 +7,7 @@ import (
     	"time"
     
     	logger "github.com/klever-io/klever-go-logger"
    -	"github.com/shirou/gopsutil/disk"
    +	"github.com/shirou/gopsutil/v4/disk"
     )
     
     var diskLog = logger.GetOrCreate("statistics/machine/disk")
    
  • core/statistics/machine/memStatistics.go+1 1 modified
    @@ -5,7 +5,7 @@ import (
     	"runtime"
     
     	"github.com/klever-io/klever-go/tools"
    -	"github.com/shirou/gopsutil/mem"
    +	"github.com/shirou/gopsutil/v4/mem"
     )
     
     // MemStatistics holds memory statistics
    
  • core/statistics/machine/netStatistics.go+1 1 modified
    @@ -8,7 +8,7 @@ import (
     	"github.com/klever-io/klever-go/data"
     	"github.com/klever-io/klever-go/eventNotifier"
     	"github.com/klever-io/klever-go/eventNotifier/notifier"
    -	"github.com/shirou/gopsutil/net"
    +	"github.com/shirou/gopsutil/v4/net"
     )
     
     // netStatistics can compute the network statistics
    
  • core/statistics/machine/netStatistics_test.go+1 1 modified
    @@ -7,7 +7,7 @@ import (
     	"testing"
     
     	"github.com/klever-io/klever-go/tools"
    -	"github.com/shirou/gopsutil/net"
    +	"github.com/shirou/gopsutil/v4/net"
     	"github.com/stretchr/testify/assert"
     )
     
    
  • core/statistics/resourceMonitor.go+2 2 modified
    @@ -11,8 +11,8 @@ import (
     	"github.com/klever-io/klever-go/core/statistics/machine"
     	"github.com/klever-io/klever-go/storage"
     	"github.com/klever-io/klever-go/tools"
    -	"github.com/shirou/gopsutil/net"
    -	"github.com/shirou/gopsutil/process"
    +	"github.com/shirou/gopsutil/v4/net"
    +	"github.com/shirou/gopsutil/v4/process"
     )
     
     // ResourceMonitor outputs statistics about resources used by the binary
    
  • go.mod+22 19 modified
    @@ -30,19 +30,19 @@ require (
     	github.com/keygen-sh/machineid v1.1.1
     	github.com/klever-io/klever-go-logger v1.3.1
     	github.com/libp2p/go-libp2p v0.48.0
    -	github.com/libp2p/go-libp2p-kad-dht v0.39.1
    +	github.com/libp2p/go-libp2p-kad-dht v0.39.2
     	github.com/libp2p/go-libp2p-kbucket v0.8.0
     	github.com/libp2p/go-libp2p-pubsub v0.15.0
     	github.com/martinlindhe/base36 v1.1.1
     	github.com/mitchellh/mapstructure v1.5.0
    -	github.com/mr-tron/base58 v1.2.0
    +	github.com/mr-tron/base58 v1.3.0
     	github.com/multiformats/go-base36 v0.2.0
     	github.com/multiformats/go-multiaddr v0.16.1
     	github.com/nwidger/jsoncolor v0.3.0
     	github.com/patrickmn/go-cache v2.1.0+incompatible
     	github.com/pelletier/go-toml v1.9.3
     	github.com/pkg/errors v0.9.1
    -	github.com/shirou/gopsutil v3.21.11+incompatible
    +	github.com/shirou/gopsutil/v4 v4.26.4
     	github.com/spf13/cobra v1.8.1
     	github.com/stretchr/testify v1.11.1
     	github.com/swaggo/files v1.0.1
    @@ -51,9 +51,9 @@ require (
     	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
     	github.com/urfave/cli v1.22.10
     	github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee
    -	golang.org/x/crypto v0.49.0
    -	golang.org/x/term v0.41.0
    -	golang.org/x/text v0.35.0
    +	golang.org/x/crypto v0.50.0
    +	golang.org/x/term v0.42.0
    +	golang.org/x/text v0.36.0
     	google.golang.org/protobuf v1.36.11
     	gopkg.in/go-playground/validator.v8 v8.18.2
     	gopkg.in/yaml.v2 v2.4.0
    @@ -76,6 +76,7 @@ require (
     	github.com/davecgh/go-spew v1.1.1 // indirect
     	github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
     	github.com/dunglas/httpsfv v1.1.0 // indirect
    +	github.com/ebitengine/purego v0.10.0 // indirect
     	github.com/elastic/elastic-transport-go/v8 v8.1.0 // indirect
     	github.com/filecoin-project/go-clock v0.1.0 // indirect
     	github.com/flynn/noise v1.1.0 // indirect
    @@ -99,10 +100,10 @@ require (
     	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
     	github.com/huin/goupnp v1.3.0 // indirect
     	github.com/inconshreveable/mousetrap v1.1.0 // indirect
    -	github.com/ipfs/boxo v0.37.0 // indirect
    -	github.com/ipfs/go-cid v0.6.0 // indirect
    +	github.com/ipfs/boxo v0.39.0 // indirect
    +	github.com/ipfs/go-cid v0.6.1 // indirect
     	github.com/ipfs/go-datastore v0.9.1 // indirect
    -	github.com/ipld/go-ipld-prime v0.22.0 // indirect
    +	github.com/ipld/go-ipld-prime v0.23.0 // indirect
     	github.com/jackpal/go-nat-pmp v1.0.2 // indirect
     	github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
     	github.com/josharian/intern v1.0.0 // indirect
    @@ -121,6 +122,7 @@ require (
     	github.com/libp2p/go-netroute v0.4.0 // indirect
     	github.com/libp2p/go-reuseport v0.4.0 // indirect
     	github.com/libp2p/go-yamux/v5 v5.0.1 // indirect
    +	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
     	github.com/mailru/easyjson v0.9.1 // indirect
     	github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
     	github.com/mattn/go-colorable v0.1.13 // indirect
    @@ -135,7 +137,7 @@ require (
     	github.com/multiformats/go-base32 v0.1.0 // indirect
     	github.com/multiformats/go-multiaddr-dns v0.5.0 // indirect
     	github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
    -	github.com/multiformats/go-multibase v0.2.0 // indirect
    +	github.com/multiformats/go-multibase v0.3.0 // indirect
     	github.com/multiformats/go-multicodec v0.10.0 // indirect
     	github.com/multiformats/go-multihash v0.2.3 // indirect
     	github.com/multiformats/go-multistream v0.6.1 // indirect
    @@ -164,6 +166,7 @@ require (
     	github.com/pion/webrtc/v4 v4.1.2 // indirect
     	github.com/pmezard/go-difflib v1.0.0 // indirect
     	github.com/polydawn/refmt v0.89.1-0.20231129105047-37766d95467a // indirect
    +	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
     	github.com/prometheus/client_golang v1.23.2 // indirect
     	github.com/prometheus/client_model v0.6.2 // indirect
     	github.com/prometheus/common v0.67.5 // indirect
    @@ -175,13 +178,13 @@ require (
     	github.com/russross/blackfriday/v2 v2.1.0 // indirect
     	github.com/spaolacci/murmur3 v1.1.0 // indirect
     	github.com/spf13/pflag v1.0.5 // indirect
    -	github.com/tklauser/go-sysconf v0.3.10 // indirect
    -	github.com/tklauser/numcpus v0.4.0 // indirect
    +	github.com/tklauser/go-sysconf v0.3.16 // indirect
    +	github.com/tklauser/numcpus v0.11.0 // indirect
     	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
     	github.com/ugorji/go/codec v1.2.12 // indirect
     	github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
     	github.com/wlynxg/anet v0.0.5 // indirect
    -	github.com/yusufpapurcu/wmi v1.2.2 // indirect
    +	github.com/yusufpapurcu/wmi v1.2.4 // indirect
     	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
     	go.opentelemetry.io/otel v1.42.0 // indirect
     	go.opentelemetry.io/otel/metric v1.42.0 // indirect
    @@ -193,14 +196,14 @@ require (
     	go.uber.org/zap v1.27.1 // indirect
     	go.yaml.in/yaml/v2 v2.4.4 // indirect
     	golang.org/x/arch v0.10.0 // indirect
    -	golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
    -	golang.org/x/mod v0.34.0 // indirect
    -	golang.org/x/net v0.52.0 // indirect
    +	golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
    +	golang.org/x/mod v0.35.0 // indirect
    +	golang.org/x/net v0.53.0 // indirect
     	golang.org/x/sync v0.20.0 // indirect
    -	golang.org/x/sys v0.42.0 // indirect
    -	golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
    +	golang.org/x/sys v0.43.0 // indirect
    +	golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
     	golang.org/x/time v0.12.0 // indirect
    -	golang.org/x/tools v0.43.0 // indirect
    +	golang.org/x/tools v0.44.0 // indirect
     	gonum.org/v1/gonum v0.17.0 // indirect
     	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
     	gopkg.in/yaml.v3 v3.0.1 // indirect
    
  • go.sum+47 41 modified
    @@ -134,6 +134,8 @@ github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54
     github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
     github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
     github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
    +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
    +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
     github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE=
     github.com/elastic/elastic-transport-go/v8 v8.1.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
     github.com/elastic/go-elasticsearch/v8 v8.4.0 h1:Rn1mcqaIMcNT43hnx2H62cIFZ+B6mjWtzj85BDKrvCE=
    @@ -334,22 +336,22 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
     github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
     github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
     github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
    -github.com/ipfs/boxo v0.37.0 h1:2E3mZvydMI2t5IkAgtkmZ3sGsld0oS7o3I+xyzDk6uI=
    -github.com/ipfs/boxo v0.37.0/go.mod h1:8yyiRn54F2CsW13n0zwXEPrVsZix/gFj9SYIRYMZ6KE=
    +github.com/ipfs/boxo v0.39.0 h1:u9jLf5pLx5SWROXjHtj8VMvv+iDlMbiTyZ/vVTQ4VhI=
    +github.com/ipfs/boxo v0.39.0/go.mod h1:k9YCvMjytFguMHndEiGdCGMMj4b7CkdOT44vtgAxOdk=
     github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk=
     github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA=
    -github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
    -github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
    +github.com/ipfs/go-cid v0.6.1 h1:T5TnNb08+ueovG76Z5gx1L4Y7QOaGTXHg1F6raWFxIc=
    +github.com/ipfs/go-cid v0.6.1/go.mod h1:zrY0SwOhjrrIdfPQ/kf+k1sXyJ0QE7cMxfCployLBs0=
     github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo=
     github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0=
     github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
     github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
     github.com/ipfs/go-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk=
     github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo=
    -github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc=
    -github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o=
    -github.com/ipld/go-ipld-prime v0.22.0 h1:YJhDhjEOvOYaqshd3b4atIWUoRg/rKrgmwCyUHwlbuY=
    -github.com/ipld/go-ipld-prime v0.22.0/go.mod h1:ol7vKxOOVgEh0iAPuiDalM+0gScXVMA5ZZa4DVrTnEA=
    +github.com/ipfs/go-test v0.3.0 h1:0Y4Uve3tp9HI+2lIJjfOliOrOgv/YpXg/l1y3P4DEYE=
    +github.com/ipfs/go-test v0.3.0/go.mod h1:JK+U8pRpATZb7lsYNSJlCj3WYB3cFfWIbI6nWRM/GFk=
    +github.com/ipld/go-ipld-prime v0.23.0 h1:csqdPZH60BsTC+AZrv7fpa27v+09I/oTqyHYYYE27eE=
    +github.com/ipld/go-ipld-prime v0.23.0/go.mod h1:46YCFSFNFBJHPjB0pfMuv7Ly7df2eChpkpyPo5SE0bA=
     github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
     github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
     github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
    @@ -409,8 +411,8 @@ github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTn
     github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk=
     github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
     github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
    -github.com/libp2p/go-libp2p-kad-dht v0.39.1 h1:9RzUBc7zywT4ZNamRSgEvPZmVlK3Y6xdlCYfXXvlR/Q=
    -github.com/libp2p/go-libp2p-kad-dht v0.39.1/go.mod h1:Po2JugFEkDq9Vig/JXtc153ntOi0q58o4j7IuITCOVs=
    +github.com/libp2p/go-libp2p-kad-dht v0.39.2 h1:L0VVfNwZnlyfS56lgtUbfevx1La9GGiEnifHndLaA40=
    +github.com/libp2p/go-libp2p-kad-dht v0.39.2/go.mod h1:DwCTwO3ZhBC3sGsAEG78D2LlAY0q00/HUHx8mwm/gYM=
     github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s=
     github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4=
     github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o=
    @@ -429,6 +431,8 @@ github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQsc
     github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
     github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg=
     github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
    +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
    +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
     github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
     github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
     github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
    @@ -490,8 +494,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
     github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
     github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
     github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
    -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
    -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
    +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
    +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
     github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
     github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
     github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
    @@ -503,8 +507,8 @@ github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5O
     github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14=
     github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
     github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
    -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
    -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
    +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68=
    +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI=
     github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc=
     github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI=
     github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
    @@ -594,6 +598,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
     github.com/polydawn/refmt v0.89.1-0.20231129105047-37766d95467a h1:cgqrm0F3zwf9IPzca7xN4w+Zy6MC9ZkPvAC8QEWa/iQ=
     github.com/polydawn/refmt v0.89.1-0.20231129105047-37766d95467a/go.mod h1:ocZfO/tLSHqfScRDNTJbAJR1by4D1lewauX9OwTaPuY=
     github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
    +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
    +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
     github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
     github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
     github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
    @@ -621,9 +627,9 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
     github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
     github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
     github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
    -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
    -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
     github.com/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
    +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
    +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
     github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
     github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
     github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
    @@ -673,11 +679,11 @@ github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4
     github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
     github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
     github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
    -github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
    -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
    +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
    +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
     github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
    -github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
    -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
    +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
    +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
     github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
     github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
     github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
    @@ -703,8 +709,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
     github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
     github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
     github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
    -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
    -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
    +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
    +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
     go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
     go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
     go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
    @@ -757,8 +763,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
     golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
     golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
     golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
    -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/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
    +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
     golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
     golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
     golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
    @@ -769,8 +775,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
     golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
     golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
     golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
    -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
    -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
    +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
    +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
     golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
     golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
     golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
    @@ -799,8 +805,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
     golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
     golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
     golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
    -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
    -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
    +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
    +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
     golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
     golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
     golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
    @@ -847,8 +853,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
     golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
     golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
     golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
    -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
    -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
    +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
    +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
     golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
     golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
     golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
    @@ -918,6 +924,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
     golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
     golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
     golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
     golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
     golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
     golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    @@ -935,22 +942,21 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
     golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
    -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
     golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
    -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/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
    -golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
    +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
    +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
    +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
    +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
     golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
     golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
     golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
     golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
    -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
    -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
    +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
    +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
     golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
     golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
     golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
    @@ -962,8 +968,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
     golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
     golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
     golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
    -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
    -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
    +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
    +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
     golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
     golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
     golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
    @@ -1025,8 +1031,8 @@ golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
     golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
     golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
     golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
    -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
    -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
    +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
    +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
     golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
     golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
     golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    
3b06f690e974

perf(kapp): zero-copy GetAndClearReturnData + pre-size receipt filters (#45)

https://github.com/klever-io/klever-goFernando SobreiraMay 12, 2026Fixed in 1.7.18via ghsa-release-walk
3 files changed · +198 13
  • core/kapp/context.go+16 13 modified
    @@ -46,7 +46,7 @@ func (r *ReceiptSlice) Get() []*transaction.Transaction_Receipt {
     }
     
     func (r *ReceiptSlice) GetByType(receiptType int8) []*transaction.Transaction_Receipt {
    -	var filtered []*transaction.Transaction_Receipt
    +	filtered := make([]*transaction.Transaction_Receipt, 0, len(*r))
     	for _, receipt := range *r {
     		if len(receipt.Data) > 0 && len(receipt.Data[0]) > 0 && int8(receipt.Data[0][0]) == receiptType {
     			filtered = append(filtered, receipt)
    @@ -56,7 +56,7 @@ func (r *ReceiptSlice) GetByType(receiptType int8) []*transaction.Transaction_Re
     }
     
     func (r *ReceiptSlice) GetPreserved() []*transaction.Transaction_Receipt {
    -	var filtered []*transaction.Transaction_Receipt
    +	filtered := make([]*transaction.Transaction_Receipt, 0, len(*r))
     	for _, receipt := range *r {
     		if len(receipt.Data) > 0 && len(receipt.Data[0]) > 0 && receipt.Data[0][0] >= SystemReceiptTypeStart {
     			filtered = append(filtered, receipt)
    @@ -150,19 +150,22 @@ func (k *kappContext) AddReturnData(data []byte) {
     }
     
     func (k *kappContext) GetAndClearReturnData() [][]byte {
    -	// Create a new outer slice with the same length as src
    -	dst := make([][]byte, len(k.returnData))
    -
    -	// Iterate over each inner slice
    -	for i, s := range k.returnData {
    -		// Create a new inner slice with the same length as s
    -		dst[i] = make([]byte, len(s))
    -		// Copy the bytes from s to dst[i]
    -		copy(dst[i], s)
    +	// Move semantics: ownership of returnData transfers to the caller and
    +	// the context resets to a fresh empty slice. SetReturnData and
    +	// AddReturnData both allocate fresh storage on every write, so no
    +	// aliasing of the returned slice remains via this struct.
    +	//
    +	// Preserve the original guarantee that this method never returns nil
    +	// and that the field is non-nil-empty after the call: VMOutputApi
    +	// carries a json:"returnData" tag, where nil JSON-renders to null
    +	// while []byte{} renders to []. Downstream API consumers may
    +	// distinguish.
    +	out := k.returnData
    +	if out == nil {
    +		out = [][]byte{}
     	}
    -
     	k.returnData = make([][]byte, 0)
    -	return dst
    +	return out
     }
     
     func (k *kappContext) GetExecData() []byte {
    
  • core/kapp/context_returndata_bench_test.go+88 0 added
    @@ -0,0 +1,88 @@
    +package kapp_test
    +
    +import (
    +	"crypto/rand"
    +	"fmt"
    +	"testing"
    +
    +	"github.com/klever-io/klever-go/core/kapp"
    +	"github.com/klever-io/klever-go/data/block"
    +)
    +
    +// genReturnData returns n byte slices of size bytes each, populated with
    +// random data so the compiler can't constant-fold the copies away.
    +func genReturnData(n, size int) [][]byte {
    +	out := make([][]byte, n)
    +	for i := range out {
    +		out[i] = make([]byte, size)
    +		_, _ = rand.Read(out[i])
    +	}
    +	return out
    +}
    +
    +// BenchmarkGetAndClearReturnData measures the per-call cost of pulling
    +// return data out of the context. Sweep across realistic SC return shapes:
    +//
    +//   - (1, 32)     — single small item (typical: assetID, proposalID, orderID)
    +//   - (5, 32)     — small array of small items (typical: minted-token IDs)
    +//   - (10, 64)    — moderate array of moderate items (e.g., transfer batch IDs)
    +//   - (50, 256)   — large array (uncommon but plausible for view fns)
    +//   - (1, 4096)   — single large item (e.g., serialized struct)
    +func BenchmarkGetAndClearReturnData(b *testing.B) {
    +	cases := []struct{ n, size int }{
    +		{1, 32},
    +		{5, 32},
    +		{10, 64},
    +		{50, 256},
    +		{1, 4096},
    +	}
    +	for _, c := range cases {
    +		b.Run(fmt.Sprintf("n=%d/size=%d", c.n, c.size), func(b *testing.B) {
    +			// Refill the context every call so each iteration has data
    +			// to drain. We measure GetAndClearReturnData *plus* the refill
    +			// (SetReturnData), then subtract the refill cost separately.
    +			data := genReturnData(c.n, c.size)
    +			ctx := kapp.NewKappContext(kapp.ArgsNewKAppContext{
    +				OriginalSender: []byte("sender"),
    +				ContractID:     0,
    +				Block:          &block.Block{},
    +			})
    +
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				ctx.SetReturnData(data)
    +				_ = ctx.GetAndClearReturnData()
    +			}
    +		})
    +	}
    +}
    +
    +// BenchmarkSetReturnData_Only isolates the refill cost so the
    +// GetAndClearReturnData number above can be interpreted (subtract this
    +// from the combined number to get the Get cost alone).
    +func BenchmarkSetReturnData_Only(b *testing.B) {
    +	cases := []struct{ n, size int }{
    +		{1, 32},
    +		{5, 32},
    +		{10, 64},
    +		{50, 256},
    +		{1, 4096},
    +	}
    +	for _, c := range cases {
    +		b.Run(fmt.Sprintf("n=%d/size=%d", c.n, c.size), func(b *testing.B) {
    +			data := genReturnData(c.n, c.size)
    +			ctx := kapp.NewKappContext(kapp.ArgsNewKAppContext{
    +				OriginalSender: []byte("sender"),
    +				ContractID:     0,
    +				Block:          &block.Block{},
    +			})
    +
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				ctx.SetReturnData(data)
    +			}
    +		})
    +	}
    +}
    
  • core/kapp/context_test.go+94 0 modified
    @@ -1,6 +1,7 @@
     package kapp_test
     
     import (
    +	"bytes"
     	"testing"
     	"time"
     
    @@ -76,6 +77,99 @@ func TestKappContext_ExecutionTimeStorage(t *testing.T) {
     	assert.Equal(t, executionTime, retrieved)
     }
     
    +// TestKappContext_ReturnData_RoundTrip exercises the returnData lifecycle:
    +// SetReturnData -> GetAndClearReturnData empties the context, the returned
    +// slice exposes the stored payload, and re-using the context after a Get
    +// works (Add/Set on a freshly cleared context behave identically to a
    +// brand-new context).
    +func TestKappContext_ReturnData_RoundTrip(t *testing.T) {
    +	t.Parallel()
    +
    +	ctx := kapp.NewKappContext(kapp.ArgsNewKAppContext{
    +		OriginalSender: []byte("sender"),
    +		ContractID:     0,
    +		Block:          &block.Block{},
    +	})
    +
    +	// initial Get on empty context returns an empty slice
    +	first := ctx.GetAndClearReturnData()
    +	assert.Empty(t, first, "fresh context has no return data")
    +
    +	payload := [][]byte{
    +		[]byte("alpha"),
    +		[]byte("beta"),
    +		[]byte("gamma"),
    +	}
    +	ctx.SetReturnData(payload)
    +
    +	out := ctx.GetAndClearReturnData()
    +	assert.Equal(t, payload, out, "Get returns the previously Set payload")
    +
    +	// context is empty after Get
    +	again := ctx.GetAndClearReturnData()
    +	assert.Empty(t, again, "context cleared after Get")
    +
    +	// reuse: Add after a Get works
    +	ctx.AddReturnData([]byte("delta"))
    +	ctx.AddReturnData([]byte("epsilon"))
    +	out2 := ctx.GetAndClearReturnData()
    +	assert.Equal(t, [][]byte{[]byte("delta"), []byte("epsilon")}, out2)
    +}
    +
    +// TestKappContext_ReturnData_GetNeverReturnsNil pins the invariant that
    +// GetAndClearReturnData always returns a non-nil slice — matching the
    +// original implementation's `make([][]byte, 0)` reset. VMOutputApi carries
    +// a `json:"returnData"` tag, so a nil slice would JSON-render as null
    +// while an empty slice renders as []; downstream API consumers can
    +// distinguish.
    +func TestKappContext_ReturnData_GetNeverReturnsNil(t *testing.T) {
    +	t.Parallel()
    +
    +	ctx := kapp.NewKappContext(kapp.ArgsNewKAppContext{
    +		OriginalSender: []byte("sender"),
    +		ContractID:     0,
    +		Block:          &block.Block{},
    +	})
    +
    +	first := ctx.GetAndClearReturnData()
    +	assert.NotNil(t, first, "Get on a fresh context returns non-nil")
    +	assert.Empty(t, first)
    +
    +	ctx.SetReturnData([][]byte{[]byte("payload")})
    +	_ = ctx.GetAndClearReturnData() // drain
    +
    +	second := ctx.GetAndClearReturnData()
    +	assert.NotNil(t, second, "Get on a drained context returns non-nil")
    +	assert.Empty(t, second)
    +}
    +
    +// TestKappContext_ReturnData_GetIsolatesFromFutureWrites guards against a
    +// regression of the move-semantics optimization: the slice returned to the
    +// caller must not be observably mutated by subsequent Set/Add on the same
    +// context (the context allocates fresh storage on each Set/Add, so the
    +// returned slice keeps pointing at the prior payload).
    +func TestKappContext_ReturnData_GetIsolatesFromFutureWrites(t *testing.T) {
    +	t.Parallel()
    +
    +	ctx := kapp.NewKappContext(kapp.ArgsNewKAppContext{
    +		OriginalSender: []byte("sender"),
    +		ContractID:     0,
    +		Block:          &block.Block{},
    +	})
    +
    +	ctx.SetReturnData([][]byte{[]byte("first")})
    +	out := ctx.GetAndClearReturnData()
    +
    +	// Subsequent context writes must not bleed into the previously
    +	// returned slice.
    +	ctx.SetReturnData([][]byte{[]byte("second"), []byte("third")})
    +	ctx.AddReturnData([]byte("fourth"))
    +
    +	assert.Equal(t, 1, len(out), "previously returned slice length is stable")
    +	assert.True(t, bytes.Equal(out[0], []byte("first")),
    +		"previously returned bytes are stable")
    +}
    +
     // TestReceiptSlice_GetByType tests filtering receipts by type
     func TestReceiptSlice_GetByType(t *testing.T) {
     	t.Parallel()
    
d695ec5c2cf1

perf(dataValidators): drop per-tx goroutine fan-out in CheckTxValidity (#44)

https://github.com/klever-io/klever-goFernando SobreiraMay 12, 2026Fixed in 1.7.18via ghsa-release-walk
2 files changed · +355 23
  • core/process/dataValidators/txValidator_bench_test.go+349 0 added
    @@ -0,0 +1,349 @@
    +package dataValidators_test
    +
    +import (
    +	"bytes"
    +	"crypto/sha256"
    +	"errors"
    +	"fmt"
    +	"sync"
    +	"testing"
    +
    +	"github.com/klever-io/klever-go/common/mock"
    +	"github.com/klever-io/klever-go/core"
    +	"github.com/klever-io/klever-go/core/process"
    +	"github.com/klever-io/klever-go/core/process/dataValidators"
    +	"github.com/klever-io/klever-go/crypto"
    +	cryptoMock "github.com/klever-io/klever-go/crypto/mock"
    +	"github.com/klever-io/klever-go/crypto/signing"
    +	"github.com/klever-io/klever-go/crypto/signing/ed25519"
    +	"github.com/klever-io/klever-go/data/state"
    +)
    +
    +var errBenchInvalidPubKey = errors.New("bench: invalid pubkey")
    +
    +// benchKeyGen returns a KeyGenMock whose PublicKeyFromByteArray simulates
    +// `cost` rounds of sha256 work per call. cost=0 returns immediately and
    +// measures pure scaffolding overhead; cost>0 performs exactly `cost`
    +// sha256 rounds to approximate real elliptic-curve point decoding.
    +func benchKeyGen(cost int) *cryptoMock.KeyGenMock {
    +	return &cryptoMock.KeyGenMock{
    +		PublicKeyFromByteArrayMock: func(b []byte) (crypto.PublicKey, error) {
    +			if cost == 0 {
    +				return &cryptoMock.PublicKeyMock{}, nil
    +			}
    +			h := sha256.Sum256(b)
    +			for i := 1; i < cost; i++ {
    +				h = sha256.Sum256(h[:])
    +			}
    +			_ = h
    +			return &cryptoMock.PublicKeyMock{}, nil
    +		},
    +	}
    +}
    +
    +// buildBenchValidator creates a tx validator whose account has `numSigners`
    +// signers, all sharing the same address. Threshold=1, each weight=1, so
    +// any one valid Verify clears the threshold check.
    +func buildBenchValidator(tb testing.TB, numSigners int, keygenCost int) (process.TxValidator, []byte) {
    +	tb.Helper()
    +	addressMock := bytes.Repeat([]byte{0xAB}, 32)
    +
    +	signers := make([]*state.Key, numSigners)
    +	for i := range signers {
    +		signers[i] = &state.Key{Address: addressMock, Weight: 1}
    +	}
    +
    +	adb := &mock.AccountsStub{
    +		GetExistingAccountCalled: func(_ []byte) (state.AccountHandler, error) {
    +			acc, err := state.NewUserAccount(addressMock)
    +			if err != nil {
    +				return nil, err
    +			}
    +			acc.Permissions = []*state.Permission{
    +				{
    +					Type:      state.Permission_Owner,
    +					Threshold: 1,
    +					Signers:   signers,
    +				},
    +			}
    +			return acc, nil
    +		},
    +	}
    +
    +	v, err := dataValidators.NewTxValidator(
    +		adb,
    +		storageTest,
    +		getTxPoolsHolder(),
    +		&mock.WhiteListHandlerStub{},
    +		mock.NewPubkeyConverterMock(32),
    +		&cryptoMock.SingleSignerStub{
    +			VerifyCalled: func(_ crypto.PublicKey, _, _ []byte) error { return nil },
    +		},
    +		benchKeyGen(keygenCost),
    +		getKAppController(),
    +		core.MaxTxNonceDeltaAllowed,
    +	)
    +	if err != nil {
    +		tb.Fatalf("NewTxValidator: %v", err)
    +	}
    +	return v, addressMock
    +}
    +
    +// makeBenchInterceptedTx wraps the validator handler + intercepted-data stub
    +// into the anonymous struct CheckTxValidity expects.
    +func makeBenchInterceptedTx(addr []byte) interface {
    +	process.InterceptedData
    +	process.TxValidatorHandler
    +} {
    +	return struct {
    +		process.InterceptedData
    +		process.TxValidatorHandler
    +	}{
    +		InterceptedData:    &mock.InterceptedDataStub{},
    +		TxValidatorHandler: getTxValidatorHandler(addr, 0, 0),
    +	}
    +}
    +
    +// BenchmarkCheckTxValidity_Signers measures end-to-end CheckTxValidity time
    +// across signer counts. The keygen cost is 0 (mock returns immediately) so
    +// the result isolates the signer-loop overhead from real EC point decode.
    +func BenchmarkCheckTxValidity_Signers(b *testing.B) {
    +	for _, n := range []int{1, 2, 3, 5, 10, 20} {
    +		b.Run(fmt.Sprintf("signers=%d", n), func(b *testing.B) {
    +			v, addr := buildBenchValidator(b, n, 0)
    +			tx := makeBenchInterceptedTx(addr)
    +
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				if err := v.CheckTxValidity(tx); err != nil {
    +					b.Fatalf("unexpected err: %v", err)
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +// BenchmarkCheckTxValidity_SignersWithCost measures the same path with a
    +// realistic per-signer cost (50 sha256 iterations ~= a few µs of CPU work
    +// per pubkey decode), which is the regime where parallelism could help.
    +func BenchmarkCheckTxValidity_SignersWithCost(b *testing.B) {
    +	for _, n := range []int{1, 3, 10} {
    +		b.Run(fmt.Sprintf("signers=%d", n), func(b *testing.B) {
    +			v, addr := buildBenchValidator(b, n, 50)
    +			tx := makeBenchInterceptedTx(addr)
    +
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				if err := v.CheckTxValidity(tx); err != nil {
    +					b.Fatalf("unexpected err: %v", err)
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +// loadSignerKeysSerial replicates the new production loop body in isolation,
    +// for direct head-to-head benching against the parallel variant.
    +func loadSignerKeysSerial(signers []*state.Key, kg crypto.KeyGenerator) (map[string]crypto.PublicKey, error) {
    +	out := make(map[string]crypto.PublicKey, len(signers))
    +	for _, s := range signers {
    +		pk, err := kg.PublicKeyFromByteArray(s.Address)
    +		if err != nil {
    +			return nil, err
    +		}
    +		out[string(s.Address)] = pk
    +	}
    +	return out, nil
    +}
    +
    +// loadSignerKeysParallel replicates the OLD production loop body (goroutine
    +// per signer + WaitGroup + Mutex). Kept here so we can bench both
    +// implementations against real ed25519 keygen and pick a breakeven.
    +func loadSignerKeysParallel(signers []*state.Key, kg crypto.KeyGenerator) (map[string]crypto.PublicKey, error) {
    +	out := make(map[string]crypto.PublicKey)
    +	var wg sync.WaitGroup
    +	var mu sync.Mutex
    +	var loadErr error
    +	var errMu sync.Mutex
    +	for _, s := range signers {
    +		wg.Add(1)
    +		go func(addrPub string) {
    +			defer wg.Done()
    +			pk, err := kg.PublicKeyFromByteArray([]byte(addrPub))
    +			if err != nil {
    +				errMu.Lock()
    +				if loadErr == nil {
    +					loadErr = err
    +				}
    +				errMu.Unlock()
    +				return
    +			}
    +			mu.Lock()
    +			out[addrPub] = pk
    +			mu.Unlock()
    +		}(string(s.Address))
    +	}
    +	wg.Wait()
    +	if loadErr != nil {
    +		return nil, loadErr
    +	}
    +	return out, nil
    +}
    +
    +// realKeyGenAndAddrs returns a real ed25519 KeyGenerator plus n valid 32-byte
    +// public-key addresses (so PublicKeyFromByteArray actually does the EC point
    +// decode work — random bytes would either fail decoding or take a degenerate
    +// fast-path).
    +func realKeyGenAndAddrs(n int) (crypto.KeyGenerator, [][]byte) {
    +	kg := signing.NewKeyGenerator(ed25519.NewEd25519())
    +	addrs := make([][]byte, n)
    +	for i := range addrs {
    +		_, pub := kg.GeneratePair()
    +		raw, err := pub.ToByteArray()
    +		if err != nil {
    +			panic(err)
    +		}
    +		addrs[i] = raw
    +	}
    +	return kg, addrs
    +}
    +
    +func makeSigners(addrs [][]byte) []*state.Key {
    +	signers := make([]*state.Key, len(addrs))
    +	for i, a := range addrs {
    +		signers[i] = &state.Key{Address: a, Weight: 1}
    +	}
    +	return signers
    +}
    +
    +// BenchmarkLoadSignerKeys_RealEd25519 measures the signer-key-decode loop in
    +// isolation, with the real ed25519 keygen used in production. Reports both
    +// implementations side-by-side so we can pin the breakeven for a possible
    +// `len(signers) > N` conditional.
    +func BenchmarkLoadSignerKeys_RealEd25519(b *testing.B) {
    +	for _, n := range []int{1, 2, 3, 4, 5, 6, 8, 10, 16, 20} {
    +		kg, addrs := realKeyGenAndAddrs(n)
    +		signers := makeSigners(addrs)
    +
    +		b.Run(fmt.Sprintf("serial/signers=%d", n), func(b *testing.B) {
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				if _, err := loadSignerKeysSerial(signers, kg); err != nil {
    +					b.Fatal(err)
    +				}
    +			}
    +		})
    +		b.Run(fmt.Sprintf("parallel/signers=%d", n), func(b *testing.B) {
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				if _, err := loadSignerKeysParallel(signers, kg); err != nil {
    +					b.Fatal(err)
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +// BenchmarkCheckTxValidity_RealEd25519 covers the end-to-end CheckTxValidity
    +// path with real ed25519 keygen, varying signer count. Confirms the
    +// loop-isolation result holds when the rest of CheckTxValidity is included.
    +func BenchmarkCheckTxValidity_RealEd25519(b *testing.B) {
    +	for _, n := range []int{1, 2, 3, 5, 10, 20} {
    +		b.Run(fmt.Sprintf("signers=%d", n), func(b *testing.B) {
    +			kg, addrs := realKeyGenAndAddrs(n)
    +			// All signers share the first address so the signature loop
    +			// finds a match immediately (we're measuring the keygen loop,
    +			// not signature verification cost).
    +			sharedAddr := addrs[0]
    +			signers := make([]*state.Key, n)
    +			for i := range signers {
    +				signers[i] = &state.Key{Address: sharedAddr, Weight: 1}
    +			}
    +			adb := &mock.AccountsStub{
    +				GetExistingAccountCalled: func(_ []byte) (state.AccountHandler, error) {
    +					acc, err := state.NewUserAccount(sharedAddr)
    +					if err != nil {
    +						return nil, err
    +					}
    +					acc.Permissions = []*state.Permission{
    +						{Type: state.Permission_Owner, Threshold: 1, Signers: signers},
    +					}
    +					return acc, nil
    +				},
    +			}
    +			v, err := dataValidators.NewTxValidator(
    +				adb, storageTest, getTxPoolsHolder(), &mock.WhiteListHandlerStub{},
    +				mock.NewPubkeyConverterMock(32),
    +				&cryptoMock.SingleSignerStub{
    +					VerifyCalled: func(_ crypto.PublicKey, _, _ []byte) error { return nil },
    +				},
    +				kg, getKAppController(), core.MaxTxNonceDeltaAllowed,
    +			)
    +			if err != nil {
    +				b.Fatalf("NewTxValidator: %v", err)
    +			}
    +			tx := makeBenchInterceptedTx(sharedAddr)
    +
    +			b.ReportAllocs()
    +			b.ResetTimer()
    +			for i := 0; i < b.N; i++ {
    +				if err := v.CheckTxValidity(tx); err != nil {
    +					b.Fatalf("unexpected err: %v", err)
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +// Smoke test: ensure the bench scaffolding actually exercises the multi-signer
    +// path. Catches breakage in the helper before the bench reports misleading
    +// numbers (e.g., short-circuit returning early because of mock misconfig).
    +func TestBenchScaffolding_MultiSignerPathExecutes(t *testing.T) {
    +	t.Parallel()
    +	for _, n := range []int{1, 3, 10} {
    +		v, addr := buildBenchValidator(t, n, 0)
    +		tx := makeBenchInterceptedTx(addr)
    +		if err := v.CheckTxValidity(tx); err != nil {
    +			t.Fatalf("signers=%d: %v", n, err)
    +		}
    +	}
    +	// also verify error path: invalid pubkey decode propagates
    +	addressMock := bytes.Repeat([]byte{0xAB}, 32)
    +	signers := []*state.Key{{Address: addressMock, Weight: 1}, {Address: addressMock, Weight: 1}}
    +	adb := &mock.AccountsStub{
    +		GetExistingAccountCalled: func(_ []byte) (state.AccountHandler, error) {
    +			acc, err := state.NewUserAccount(addressMock)
    +			if err != nil {
    +				return nil, err
    +			}
    +			acc.Permissions = []*state.Permission{{Type: state.Permission_Owner, Threshold: 1, Signers: signers}}
    +			return acc, nil
    +		},
    +	}
    +	v, err := dataValidators.NewTxValidator(
    +		adb,
    +		storageTest,
    +		getTxPoolsHolder(),
    +		&mock.WhiteListHandlerStub{},
    +		mock.NewPubkeyConverterMock(32),
    +		&cryptoMock.SingleSignerStub{},
    +		&cryptoMock.KeyGenMock{
    +			PublicKeyFromByteArrayMock: func(_ []byte) (crypto.PublicKey, error) {
    +				return nil, errBenchInvalidPubKey
    +			},
    +		},
    +		getKAppController(),
    +		core.MaxTxNonceDeltaAllowed,
    +	)
    +	if err != nil {
    +		t.Fatalf("NewTxValidator: %v", err)
    +	}
    +	tx := makeBenchInterceptedTx(addressMock)
    +	if err := v.CheckTxValidity(tx); !errors.Is(err, errBenchInvalidPubKey) {
    +		t.Fatalf("expected %v, got %v", errBenchInvalidPubKey, err)
    +	}
    +}
    
  • core/process/dataValidators/txValidator.go+6 23 modified
    @@ -2,7 +2,6 @@ package dataValidators
     
     import (
     	"fmt"
    -	"sync"
     
     	logger "github.com/klever-io/klever-go-logger"
     	"github.com/klever-io/klever-go/common"
    @@ -184,29 +183,13 @@ func (txv *txValidator) CheckTxValidity(interceptedTx process.TxValidatorHandler
     		}
     	}
     
    -	signersPub := make(map[string]crypto.PublicKey)
    -	wait := sync.WaitGroup{}
    -	var mu sync.Mutex
    -	var loadErr error
    +	signersPub := make(map[string]crypto.PublicKey, len(permission.Signers))
     	for _, signer := range permission.Signers {
    -		wait.Add(1)
    -		go func(addrPub string, wg *sync.WaitGroup, sig map[string]crypto.PublicKey, mut *sync.Mutex) {
    -			defer wg.Done()
    -			senderPubKey, err := txv.keyGen.PublicKeyFromByteArray([]byte(addrPub))
    -			if err != nil {
    -				// signer address is invalid
    -				loadErr = err
    -				return
    -			}
    -			mut.Lock()
    -			sig[addrPub] = senderPubKey
    -			mut.Unlock()
    -		}(string(signer.Address), &wait, signersPub, &mu)
    -	}
    -
    -	wait.Wait()
    -	if loadErr != nil {
    -		return loadErr
    +		senderPubKey, err := txv.keyGen.PublicKeyFromByteArray(signer.Address)
    +		if err != nil {
    +			return err
    +		}
    +		signersPub[string(signer.Address)] = senderPubKey
     	}
     
     	signWeight := int64(0)
    

Vulnerability mechanics

Root cause

"The Batch.Decompress function does not enforce a cap on the number of decoded items, allowing a small compressed request to expand into a large number of entries."

Attack vector

A connected peer can send a compressed `RequestDataType_HashArrayType` direct request. This request is small on the wire but expands significantly after decompression within the resolver path. The `TxResolver` and `TrieNodeResolver` then process this large set of decoded hashes, leading to resource exhaustion [ref_id=2].

Affected code

The vulnerability lies in the `Batch.Decompress` function in `data/batch/batch.go` [ref_id=2], which fails to limit the number of decoded items. Subsequently, `TxResolver` in `data/retriever/resolvers/transactionResolver.go` and `TrieNodeResolver` in `data/retriever/resolvers/trieNodeResolver.go` iterate over these unchecked decoded hashes, leading to resource exhaustion [ref_id=2].

What the fix does

The patch introduces a hard cap, `batch.MaxItemsPerBatch`, to limit the number of decoded items in a batch. This cap is enforced at multiple layers: `Batch.Decompress` rejects batches exceeding the cap, and `TxResolver` and `TrieNodeResolver` check the cap immediately after unmarshalling. This prevents both compressed and uncompressed item-count bombs from causing excessive memory and CPU amplification [patch_id=4935400].

Preconditions

  • networkThe target node must accept P2P peer connections.
  • inputThe attacker must send a specially crafted compressed `RequestDataType_HashArrayType` direct request.

Reproduction

```bash go run auditpoc/request_batch_hash_amplification_poc.go go test github.com/klever-io/klever-go/auditpoc -run TestRequestBatchHashAmplification_DirectSendReachability -count=1 ```

Generated on Jun 5, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.