Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go/ct/driver/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/0xsoniclabs/tosca/go/interpreter/evmzero"
"github.com/0xsoniclabs/tosca/go/interpreter/geth"
"github.com/0xsoniclabs/tosca/go/interpreter/lfvm"
"github.com/0xsoniclabs/tosca/go/interpreter/sfvm"
"github.com/0xsoniclabs/tosca/go/lib/cpp"
"github.com/0xsoniclabs/tosca/go/lib/rust"
"github.com/0xsoniclabs/tosca/go/tosca"
Expand Down Expand Up @@ -57,6 +58,7 @@ var RunCmd = cliUtils.AddCommonFlags(cli.Command{

var evms = map[string]ct.Evm{
"lfvm": lfvm.NewConformanceTestingTarget(),
"sfvm": sfvm.NewConformanceTestingTarget(),
"geth": geth.NewConformanceTestingTarget(),
"evmzero": evmzero.NewConformanceTestingTarget(),
"evmrs": evmrs.NewConformanceTestingTarget(),
Expand Down
6 changes: 6 additions & 0 deletions go/ct/evm_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/0xsoniclabs/tosca/go/interpreter/evmzero"
"github.com/0xsoniclabs/tosca/go/interpreter/geth"
"github.com/0xsoniclabs/tosca/go/interpreter/lfvm"
"github.com/0xsoniclabs/tosca/go/interpreter/sfvm"
"github.com/0xsoniclabs/tosca/go/tosca"
)

Expand All @@ -38,6 +39,11 @@ func FuzzLfvm(f *testing.F) {
fuzzVm(lfvm.NewConformanceTestingTarget(), f)
}

// FuzzSfvm is a fuzzing test for sfvm
func FuzzSfvm(f *testing.F) {
fuzzVm(sfvm.NewConformanceTestingTarget(), f)
}

// FuzzLfvm is a fuzzing test for evmzero
func FuzzEvmzero(f *testing.F) {
fuzzVm(evmzero.NewConformanceTestingTarget(), f)
Expand Down
2 changes: 2 additions & 0 deletions go/ct/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import (
"github.com/0xsoniclabs/tosca/go/interpreter/evmzero"
"github.com/0xsoniclabs/tosca/go/interpreter/geth"
"github.com/0xsoniclabs/tosca/go/interpreter/lfvm"
"github.com/0xsoniclabs/tosca/go/interpreter/sfvm"
"github.com/0xsoniclabs/tosca/go/tosca"
"github.com/0xsoniclabs/tosca/go/tosca/vm"
)

var evms = map[string]ct.Evm{
"geth": geth.NewConformanceTestingTarget(),
"lfvm": lfvm.NewConformanceTestingTarget(),
"sfvm": sfvm.NewConformanceTestingTarget(),
"evmzero": evmzero.NewConformanceTestingTarget(),
}

Expand Down
1 change: 1 addition & 0 deletions go/integration_test/interpreter/test_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
_ "github.com/0xsoniclabs/tosca/go/interpreter/evmzero"
_ "github.com/0xsoniclabs/tosca/go/interpreter/geth"
"github.com/0xsoniclabs/tosca/go/interpreter/lfvm"
_ "github.com/0xsoniclabs/tosca/go/interpreter/sfvm"
"github.com/0xsoniclabs/tosca/go/tosca"
"golang.org/x/exp/maps"
)
Expand Down
2 changes: 1 addition & 1 deletion go/integration_test/interpreter/test_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func TestCoveredVariants_ContainsMainConfigurations(t *testing.T) {
all := getAllInterpreterVariantsForTests()
wanted := []string{
"geth", "lfvm", "lfvm-si", "evmzero", "evmone",
"geth", "lfvm", "lfvm-si", "evmzero", "evmone", "sfvm",
}
for _, n := range wanted {
if !slices.Contains(all, n) {
Expand Down
1 change: 1 addition & 0 deletions go/integration_test/interpreter/test_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
_ "github.com/0xsoniclabs/tosca/go/interpreter/evmone"
_ "github.com/0xsoniclabs/tosca/go/interpreter/evmzero"
_ "github.com/0xsoniclabs/tosca/go/interpreter/lfvm"
_ "github.com/0xsoniclabs/tosca/go/interpreter/sfvm"
"github.com/0xsoniclabs/tosca/go/tosca"
)

Expand Down
7 changes: 4 additions & 3 deletions go/integration_test/processor/processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

_ "github.com/0xsoniclabs/tosca/go/interpreter/evmzero" // < registers evmzero interpreter for testing
_ "github.com/0xsoniclabs/tosca/go/interpreter/lfvm" // < registers lfvm interpreter for testing
_ "github.com/0xsoniclabs/tosca/go/interpreter/sfvm" // < registers sfvm interpreter for testing
)

// This file contains a few initial shake-down tests or a Processor implementation.
Expand Down Expand Up @@ -220,9 +221,9 @@ func TestGetProcessors_ContainsMainConfigurations(t *testing.T) {
// and interpreters are registered and available for testing.
all := maps.Keys(getProcessors())
wanted := []string{
"opera/geth", "opera/lfvm", "opera/evmzero",
"floria/geth", "floria/lfvm", "floria/evmzero",
"geth-sonic/geth", "geth-sonic/lfvm", "geth-sonic/evmzero",
"opera/geth", "opera/lfvm", "opera/sfvm", "opera/evmzero",
"floria/geth", "floria/lfvm", "floria/sfvm", "floria/evmzero",
"geth-sonic/geth", "geth-sonic/lfvm", "geth-sonic/sfvm", "geth-sonic/evmzero",
}
for _, n := range wanted {
if !slices.Contains(all, n) {
Expand Down
97 changes: 97 additions & 0 deletions go/interpreter/sfvm/analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2025 Sonic Operations Ltd
//
// Use of this software is governed by the Business Source License included
// in the LICENSE file and at soniclabs.com/bsl11.
//
// Change Date: 2028-4-16
//
// On the date above, in accordance with the Business Source License, use of
// this software will be governed by the GNU Lesser General Public License v3.

package sfvm

import (
"github.com/0xsoniclabs/tosca/go/tosca"
"github.com/0xsoniclabs/tosca/go/tosca/vm"
lru "github.com/hashicorp/golang-lru/v2"
)

type analysis struct {
cache *lru.Cache[tosca.Hash, *jumpDestMap]
}

func newAnalysis(size int) analysis {
cache, err := lru.New[tosca.Hash, *jumpDestMap](size)
if err != nil {
panic("failed to create analysis cache: " + err.Error())
}
return analysis{cache: cache}
}

func (a *analysis) analyzeJumpDest(code tosca.Code, codehash *tosca.Hash) *jumpDestMap {
if a == nil || a.cache == nil || codehash == nil {
return jumpDestAnalysisInternal(code)
}

if analysis, ok := a.cache.Get(*codehash); ok {
return analysis
}

jumpDests := jumpDestAnalysisInternal(code)
a.cache.Add(*codehash, jumpDests)
return jumpDests
}

type jumpDestMap struct {
bitmap []uint64
codeSize uint64
}

func newJumpDestMap(size uint64) *jumpDestMap {
analysisSize := size/64 + 1
analysis := &jumpDestMap{
bitmap: make([]uint64, analysisSize),
codeSize: size,
}
return analysis
}

func jumpDestAnalysisInternal(code tosca.Code) *jumpDestMap {
analysis := newJumpDestMap(uint64(len(code)))
for idx := 0; idx < len(code); idx++ {
op := vm.OpCode(code[idx])
if op >= vm.PUSH1 && op <= vm.PUSH32 {
// PUSH1 to PUSH32
dataSize := int(op) - int(vm.PUSH1) + 1
idx += dataSize // Skip the pushed data
continue
}
if op == vm.JUMPDEST {
analysis.markJumpDest(uint64(idx))
}
}
return analysis
}

func (a *jumpDestMap) isJumpDest(idx uint64) bool {
if a == nil {
return false
}
if idx >= a.codeSize {
return false
}
uintIdx, mask := idxToAnalysisIdxAndMask(idx)
return a.bitmap[uintIdx]&mask != 0
}

func (a *jumpDestMap) markJumpDest(idx uint64) {
if idx >= uint64(a.codeSize) {
return
}
uintIdx, mask := idxToAnalysisIdxAndMask(idx)
a.bitmap[uintIdx] |= mask
}

func idxToAnalysisIdxAndMask(idx uint64) (uint64, uint64) {
return idx / 64, 1 << (idx % 64)
}
108 changes: 108 additions & 0 deletions go/interpreter/sfvm/analysis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2025 Sonic Operations Ltd
//
// Use of this software is governed by the Business Source License included
// in the LICENSE file and at soniclabs.com/bsl11.
//
// Change Date: 2028-4-16
//
// On the date above, in accordance with the Business Source License, use of
// this software will be governed by the GNU Lesser General Public License v3.

package sfvm

import (
"testing"

"github.com/0xsoniclabs/tosca/go/tosca"
"github.com/0xsoniclabs/tosca/go/tosca/vm"
"github.com/stretchr/testify/require"
)

func TestAnalysisCache_PanicsOnNegativeSize(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("expected createAnalysisCache to panic on negative size")
}
}()
_ = newAnalysis(-1)
}

func TestAnalysis_NewAnalysisIsNonEmpty(t *testing.T) {
a := newJumpDestMap(10)
if a.codeSize == 0 {
t.Error("expected newAnalysis to return a non-empty Analysis")
}
if len(a.bitmap) == 0 {
t.Error("expected newAnalysis to return a non-empty data slice")
}
}

func TestAnalysis_MarkJumpDestAndIsJumpDest(t *testing.T) {
size := 10
a := newJumpDestMap(uint64(size))
a.markJumpDest(2)
a.markJumpDest(18)
// Check that the jump destination is marked correctly over boundaries
for i := 0; i < 2*size; i++ {
if i == 2 && !a.isJumpDest(uint64(i)) {
t.Errorf("expected index %d to be marked as jump destination", i)
}
if i != 2 && a.isJumpDest(uint64(i)) {
t.Errorf("expected index %d to not be marked as jump destination", i)
}
}
}

func TestAnalysis_MarksJumpDestAtCorrectIndex(t *testing.T) {
code := tosca.Code{byte(vm.JUMPDEST), byte(vm.PUSH1), byte(vm.JUMPDEST), byte(vm.JUMPDEST)}
analysis := jumpDestAnalysisInternal(code)
if !analysis.isJumpDest(0) {
t.Errorf("expected index 0 to be jump destination")
}
if analysis.isJumpDest(1) {
t.Errorf("expected index 1 to not be jump destination")
}
if analysis.isJumpDest(2) {
t.Errorf("expected index 2 to not be jump destination")
}
if !analysis.isJumpDest(3) {
t.Errorf("expected index 3 to be jump destination")
}
}

func TestAnalysis_PushDataIsSkipped(t *testing.T) {
code := tosca.Code{
byte(vm.PUSH9), byte(vm.JUMPDEST), byte(vm.JUMPDEST), byte(vm.JUMPDEST), byte(vm.JUMPDEST),
byte(vm.JUMPDEST), byte(vm.JUMPDEST), byte(vm.JUMPDEST), byte(vm.JUMPDEST), byte(vm.JUMPDEST),
byte(vm.JUMPDEST),
byte(vm.PUSH2), byte(vm.JUMPDEST), byte(vm.JUMPDEST),
byte(vm.JUMPDEST),
}
analysis := jumpDestAnalysisInternal(code)
for i := range code {
if analysis.isJumpDest(uint64(i)) && (i != 10 && i != 14) {
t.Errorf("expected index %d to be jump destination", i)
}
if !analysis.isJumpDest(uint64(i)) && (i == 10 || i == 14) {
t.Errorf("expected index %d to not be jump destination", i)
}
}
}

func TestAnalysis_InputsAreCachedUsingCodeHashAsKey(t *testing.T) {
analysis := newAnalysis(1 << 2)

code := []byte{byte(vm.STOP)}
hash := tosca.Hash{byte(1)}

want := analysis.analyzeJumpDest(code, &hash)
got := analysis.analyzeJumpDest(code, &hash)
if &want.bitmap != &got.bitmap { // < needs to be the same slice
t.Errorf("cached conversion result not returned")
}
}

func TestAnalysis_IsJumpDestCanHandleUninitializedMap(t *testing.T) {
var jumpDestMap *jumpDestMap
require.False(t, jumpDestMap.isJumpDest(uint64(0)), "expected isJumpDest to return false for uninitialized map")
}
Loading