Skip to content

Commit

Permalink
state: added forkState
Browse files Browse the repository at this point in the history
  • Loading branch information
lmittmann committed Jul 16, 2023
1 parent e29bd2b commit c66a86d
Show file tree
Hide file tree
Showing 3 changed files with 408 additions and 0 deletions.
138 changes: 138 additions & 0 deletions w3vm/state/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package state

import (
"encoding/json"
"errors"
"os"
"path/filepath"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"golang.org/x/sync/singleflight"
)

type forkState struct {
Accounts map[common.Address]*Account `json:"accounts,omitempty"`
HeaderHashes map[hexutil.Uint64]common.Hash `json:"headerHashes,omitempty"`
}

func (s *forkState) Merge(s2 *forkState) (changed bool) {
if s2 == nil || len(s2.Accounts) == 0 && len(s2.HeaderHashes) == 0 {
return false
}

// merge accounts
if s.Accounts == nil {
s.Accounts = s2.Accounts
changed = changed || len(s2.Accounts) > 0
} else {
for addrS2, accS2 := range s2.Accounts {
if accS1, ok := s.Accounts[addrS2]; ok {
if accS1.Storage == nil {
accS1.Storage = accS2.Storage
changed = changed || len(accS2.Storage) > 0
}

for slotS2, valS2 := range accS2.Storage {
if _, ok := accS1.Storage[slotS2]; ok {
continue
}

accS1.Storage[slotS2] = valS2
changed = true
}
continue
}
changed = true
s.Accounts[addrS2] = accS2
}
}

// merge header hashes
if s.HeaderHashes == nil {
s.HeaderHashes = s2.HeaderHashes
changed = changed || len(s2.HeaderHashes) > 0
} else {
for blockNumber, hash := range s2.HeaderHashes {
if _, ok := s.HeaderHashes[blockNumber]; ok {
continue
}
changed = true
s.HeaderHashes[blockNumber] = hash
}
}
return
}

var readGroup = new(singleflight.Group)

func readTestdataState(fp string) (*forkState, error) {
forkStateAny, err, _ := readGroup.Do(fp, func() (any, error) {
f, err := os.Open(fp)
if errors.Is(err, os.ErrNotExist) {
return &forkState{}, nil
} else if err != nil {
return nil, err
}
defer f.Close()

var s *forkState
if err := json.NewDecoder(f).Decode(&s); err != nil {
return nil, err
}
return s, nil
})
if err != nil {
return nil, err
}
return forkStateAny.(*forkState), nil
}

var writeGroup = new(singleflight.Group)

func writeTestdataState(fp string, s *forkState) error {
Retry:
_, err, shared := writeGroup.Do(fp, func() (any, error) {
// read current testdata state
testdataState, err := readTestdataState(fp)
if err != nil {
return nil, err
}

// merge states
if testdataState == nil {
testdataState = new(forkState)
}
if changed := testdataState.Merge(s); !changed {
return nil, nil
}

dirPath := filepath.Dir(fp)
if _, err := os.Stat(dirPath); errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(dirPath, 0775); err != nil {
return nil, err
}
}

// persist new state
f, err := os.OpenFile(fp, os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
return nil, err
}
defer f.Close()

dec := json.NewEncoder(f)
dec.SetIndent("", "\t")
if err := dec.Encode(testdataState); err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return err
}
if shared {
goto Retry
}
return nil
}
224 changes: 224 additions & 0 deletions w3vm/state/state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package state

import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/holiman/uint256"
)

var (
uint1 = *uint256.NewInt(1)
uint2 = *uint256.NewInt(2)
)

func TestReadTestdataState(t *testing.T) {
t.Run("read non-existent", func(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "1_0.json")

gotState, err := readTestdataState(fp)
if err != nil {
t.Fatalf("Failed to read state: %v", err)
}

wantState := new(forkState)
if diff := cmp.Diff(wantState, gotState); diff != "" {
t.Fatalf("(-want +got):\n%s", diff)
}
})

t.Run("read", func(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "1_0.json")

stateContent := []byte(`{"accounts":{"0x0100000000000000000000000000000000000000":{"balance":"0x1"}}}`)
if err := os.WriteFile(fp, stateContent, 0644); err != nil {
t.Fatalf("Failed to create state file: %v", err)

}

gotState, err := readTestdataState(fp)
if err != nil {
t.Fatalf("Failed to read state: %q", err)
}

wantState := &forkState{
Accounts: map[common.Address]*Account{
{0x1}: {Balance: uint1},
},
}
if diff := cmp.Diff(wantState, gotState,
cmpopts.EquateEmpty(),
); diff != "" {
t.Fatalf("(-want +got):\n%s", diff)
}
})
}

func TestWriteTestdataState(t *testing.T) {
t.Run("write non-existent", func(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "1_0.json")
wantState := &forkState{
Accounts: map[common.Address]*Account{
{0x1}: {Balance: uint1},
},
}

if err := writeTestdataState(fp, wantState); err != nil {
t.Fatalf("Failed to write state: %v", err)
}

gotState, err := readTestdataState(fp)
if err != nil {
t.Fatalf("Failed to read state: %q", err)
}
if diff := cmp.Diff(wantState, gotState,
cmpopts.EquateEmpty(),
); diff != "" {
t.Fatalf("(-want +got):\n%s", diff)
}
})

t.Run("write", func(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "1_0.json")
preState := &forkState{
Accounts: map[common.Address]*Account{
{0x1}: {Balance: uint1},
},
}
newState := &forkState{
Accounts: map[common.Address]*Account{
{0x2}: {Balance: uint2},
},
}
wantState := &forkState{
Accounts: map[common.Address]*Account{
{0x1}: {Balance: uint1},
{0x2}: {Balance: uint2},
},
}

if err := writeTestdataState(fp, preState); err != nil {
t.Fatalf("Failed to write pre-state: %v", err)
}
if err := writeTestdataState(fp, newState); err != nil {
t.Fatalf("Failed to write new-state: %v", err)
}

gotState, err := readTestdataState(fp)
if err != nil {
t.Fatalf("Failed to read state: %q", err)
}
if diff := cmp.Diff(wantState, gotState,
cmpopts.EquateEmpty(),
); diff != "" {
t.Fatalf("(-want +got):\n%s", diff)
}
})
}

func TestForkStateMerge(t *testing.T) {
tests := []struct {
S1 *forkState
S2 *forkState

Want *forkState
WantChanged bool
}{
{
S1: &forkState{},
S2: &forkState{},
Want: &forkState{},
WantChanged: false,
},
{
S1: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
S2: &forkState{},
Want: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
WantChanged: false,
},
{
S1: &forkState{},
S2: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
Want: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
WantChanged: true,
},
{
S1: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
S2: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
Want: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
WantChanged: false,
},
{
S1: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}}},
S2: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{2: {0x2}}},
Want: &forkState{HeaderHashes: map[hexutil.Uint64]common.Hash{1: {0x1}, 2: {0x2}}},
WantChanged: true,
},
{ // If the same key is present in both states, the value of S1 is NOT changed.
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Balance: uint1}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Balance: uint2}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Balance: uint1}}},
WantChanged: false,
},
{
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
WantChanged: true,
},
{
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
WantChanged: false,
},
{
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{}}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
WantChanged: true,
},
{
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{}}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{uint1: uint1}}}},
WantChanged: false,
},
{
S1: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{
uint1: uint1,
}}}},
S2: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{
uint2: uint2,
}}}},
Want: &forkState{Accounts: map[common.Address]*Account{{0x1}: {Storage: map[uint256.Int]uint256.Int{
uint1: uint1,
uint2: uint2,
}}}},
WantChanged: true,
},
}

for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
gotChanged := test.S1.Merge(test.S2)
if test.WantChanged != gotChanged {
t.Errorf("Changed: want %v, got %v", test.WantChanged, gotChanged)
}

if diff := cmp.Diff(test.Want, test.S1); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
})
}
}
Loading

0 comments on commit c66a86d

Please sign in to comment.