2026-05-31 02:56:55 -07:00
|
|
|
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
|
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
|
|
|
|
|
|
package state
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"math"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
|
|
|
|
"github.com/luxfi/node/vms/components/gas"
|
2026-06-06 02:55:23 -07:00
|
|
|
"github.com/luxfi/node/vms/pcodecs"
|
2026-05-31 02:56:55 -07:00
|
|
|
"github.com/luxfi/node/vms/platformvm/block"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestMultiVersionUnmarshalAcceptsBothVersions documents that the
|
|
|
|
|
// defensive helper is a transparent pass-through for the canonical
|
|
|
|
|
// codec — neither v0 nor v1 reads should observe any behavioral
|
|
|
|
|
// difference from raw codec.Manager.Unmarshal once the codec has both
|
|
|
|
|
// versions registered.
|
|
|
|
|
func TestMultiVersionUnmarshalAcceptsBothVersions(t *testing.T) {
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
original := gas.State{Capacity: 100, Excess: 200}
|
|
|
|
|
|
|
|
|
|
for _, version := range []uint16{block.CodecVersionV0, block.CodecVersionV1} {
|
|
|
|
|
bytesAtVersion, err := block.GenesisCodec.Marshal(version, original)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
var decoded gas.State
|
|
|
|
|
gotVersion, err := multiVersionUnmarshal(block.GenesisCodec, bytesAtVersion, &decoded)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
require.Equal(version, gotVersion)
|
|
|
|
|
require.Equal(original, decoded)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMultiVersionUnmarshalSurfacesSingleVersionCodec is the test for
|
|
|
|
|
// the soft warning path. We build a single-version codec.Manager (only
|
|
|
|
|
// v1 registered) and call multiVersionUnmarshal on it. The Unmarshal
|
|
|
|
|
// itself MUST still succeed for v1 bytes (the helper is a soft guard,
|
|
|
|
|
// not a hard one), but the post-condition probe MUST observe the
|
|
|
|
|
// missing v0 slot.
|
|
|
|
|
//
|
|
|
|
|
// We can't directly intercept log.Warn (it's a global logger), so we
|
|
|
|
|
// instead exercise the underlying probe deterministically: marshaling
|
|
|
|
|
// the empty `probe` struct at v0 on a single-version codec must return
|
2026-06-06 02:55:23 -07:00
|
|
|
// pcodecs.ErrUnknownVersion. This is the exact condition that triggers
|
2026-05-31 02:56:55 -07:00
|
|
|
// the warning emission.
|
|
|
|
|
func TestMultiVersionUnmarshalSurfacesSingleVersionCodec(t *testing.T) {
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Build a single-version codec by hand — only v1 registered.
|
2026-06-06 02:55:23 -07:00
|
|
|
singleC := pcodecs.NewLinearCodec()
|
|
|
|
|
singleVersion := pcodecs.NewDefaultManager()
|
2026-05-31 02:56:55 -07:00
|
|
|
require.NoError(singleVersion.RegisterCodec(block.CodecVersionV1, singleC))
|
|
|
|
|
|
|
|
|
|
// Confirm v1 Unmarshal still succeeds (helper is non-blocking).
|
|
|
|
|
v1Bytes, err := singleVersion.Marshal(block.CodecVersionV1, gas.State{Capacity: 1, Excess: 2})
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
var decoded gas.State
|
|
|
|
|
_, err = multiVersionUnmarshal(singleVersion, v1Bytes, &decoded)
|
|
|
|
|
require.NoError(err, "multiVersionUnmarshal must not block successful v1 reads on a single-version codec")
|
|
|
|
|
require.Equal(uint16(2), uint16(decoded.Excess))
|
|
|
|
|
|
|
|
|
|
// Confirm the underlying detection mechanism would fire: v0
|
|
|
|
|
// Marshal on this codec returns ErrUnknownVersion, which is what
|
|
|
|
|
// the post-condition check picks up.
|
|
|
|
|
_, err = singleVersion.Marshal(block.CodecVersionV0, probe{})
|
2026-06-06 02:55:23 -07:00
|
|
|
require.ErrorIs(err, pcodecs.ErrUnknownVersion,
|
2026-05-31 02:56:55 -07:00
|
|
|
"single-version codec must surface ErrUnknownVersion on the v0 probe — this is the warning trigger")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMultiVersionUnmarshalSurfacesSingleVersionCodec_Idempotent
|
|
|
|
|
// documents that the warning probe fires AT MOST once per codec
|
|
|
|
|
// pointer, so a long-running canary boot that hits dozens of state
|
|
|
|
|
// reads through the same single-version codec produces a single log
|
|
|
|
|
// line, not a flood.
|
|
|
|
|
func TestMultiVersionUnmarshalSurfacesSingleVersionCodec_Idempotent(t *testing.T) {
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
2026-06-06 02:55:23 -07:00
|
|
|
singleC := pcodecs.NewLinearCodec()
|
|
|
|
|
singleVersion := pcodecs.NewDefaultManager()
|
2026-05-31 02:56:55 -07:00
|
|
|
require.NoError(singleVersion.RegisterCodec(block.CodecVersionV1, singleC))
|
|
|
|
|
|
|
|
|
|
v1Bytes, err := singleVersion.Marshal(block.CodecVersionV1, gas.State{Capacity: 5, Excess: 6})
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
// First call: warning probe runs (sync.Map records the codec).
|
|
|
|
|
var d1 gas.State
|
|
|
|
|
_, err = multiVersionUnmarshal(singleVersion, v1Bytes, &d1)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
// Confirm the sync.Map remembers this codec — the LoadOrStore
|
|
|
|
|
// returns observed=true on subsequent calls.
|
|
|
|
|
_, observedAgain := multiVersionProbeOnce.LoadOrStore(singleVersion, struct{}{})
|
|
|
|
|
require.True(observedAgain, "probe must record the codec on first observation")
|
|
|
|
|
|
|
|
|
|
// Second call: warning probe SKIPS the marshal probe entirely.
|
|
|
|
|
// Unmarshal still succeeds.
|
|
|
|
|
var d2 gas.State
|
|
|
|
|
_, err = multiVersionUnmarshal(singleVersion, v1Bytes, &d2)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMultiVersionUnmarshalRejectsUnknownVersionUpstream confirms the
|
2026-06-06 02:55:23 -07:00
|
|
|
// helper does NOT swallow pcodecs.ErrUnknownVersion — if the input bytes
|
2026-05-31 02:56:55 -07:00
|
|
|
// carry a prefix that the codec does not have registered (e.g. a future
|
|
|
|
|
// codec v2 prefix on a v0+v1 codec), the original error surfaces so the
|
|
|
|
|
// caller can decide whether to treat it as a corruption or a soft skip.
|
|
|
|
|
func TestMultiVersionUnmarshalRejectsUnknownVersionUpstream(t *testing.T) {
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
bogus := make([]byte, 18)
|
|
|
|
|
binary.BigEndian.PutUint16(bogus[:2], math.MaxUint16)
|
|
|
|
|
|
|
|
|
|
var sink gas.State
|
|
|
|
|
_, err := multiVersionUnmarshal(block.GenesisCodec, bogus, &sink)
|
2026-06-06 02:55:23 -07:00
|
|
|
require.ErrorIs(err, pcodecs.ErrUnknownVersion)
|
2026-05-31 02:56:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestBlockGenesisCodecPassesMultiVersionProbe pins the post-v1.28.2
|
|
|
|
|
// invariant: the canonical state-side codec MUST register both v0 and
|
|
|
|
|
// v1. If a future refactor accidentally drops a version slot, this test
|
|
|
|
|
// trips immediately and the canary doesn't have to.
|
|
|
|
|
func TestBlockGenesisCodecPassesMultiVersionProbe(t *testing.T) {
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
for _, version := range []uint16{block.CodecVersionV0, block.CodecVersionV1} {
|
|
|
|
|
_, err := block.GenesisCodec.Marshal(version, probe{})
|
|
|
|
|
require.NoErrorf(err, "block.GenesisCodec must register version %d", version)
|
|
|
|
|
}
|
|
|
|
|
}
|