diff --git a/pkg/core/native/gas_record.go b/pkg/core/native/gas_record.go new file mode 100644 index 000000000..1c9121ac1 --- /dev/null +++ b/pkg/core/native/gas_record.go @@ -0,0 +1,13 @@ +package native + +import "math/big" + +// gasIndexPair contains block index together with generated gas per block. +// It is used to cache NEO GASRecords. +type gasIndexPair struct { + Index uint32 + GASPerBlock big.Int +} + +// gasRecord contains history of gas per block changes. It is used only by NEO cache. +type gasRecord []gasIndexPair diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 43830e1cc..941c33899 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -2,7 +2,9 @@ package native import ( "crypto/elliptic" + "encoding/binary" "errors" + "fmt" "math/big" "sort" "strings" @@ -189,13 +191,15 @@ func (n *NEO) Initialize(ic *interop.Context) error { } n.mint(ic, h, big.NewInt(NEOTotalSupply)) - gr := &state.GASRecord{{Index: 0, GASPerBlock: *big.NewInt(5 * GASFactor)}} - n.gasPerBlock.Store(*gr) - n.gasPerBlockChanged.Store(false) - err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixGASPerBlock}, &state.StorageItem{Value: gr.Bytes()}) + var index uint32 = 0 + value := big.NewInt(5 * GASFactor) + err = n.putGASRecord(ic.DAO, index, value) if err != nil { return err } + gr := &gasRecord{{Index: index, GASPerBlock: *value}} + n.gasPerBlock.Store(*gr) + n.gasPerBlockChanged.Store(false) err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixVotersCount}, &state.StorageItem{Value: []byte{}}) if err != nil { return err @@ -217,9 +221,8 @@ func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { return err } - var gr state.GASRecord - si = d.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock}) - if err := gr.FromBytes(si.Value); err != nil { + gr, err := n.getSortedGASRecordFromDAO(d) + if err != nil { return err } n.gasPerBlock.Store(gr) @@ -293,7 +296,10 @@ func (n *NEO) PostPersist(ic *interop.Context) error { // OnPersistEnd updates cached values if they've been changed. func (n *NEO) OnPersistEnd(d dao.DAO) { if n.gasPerBlockChanged.Load().(bool) { - gr := n.getGASRecordFromDAO(d) + gr, err := n.getSortedGASRecordFromDAO(d) + if err != nil { + panic(err) + } n.gasPerBlock.Store(gr) n.gasPerBlockChanged.Store(false) } @@ -365,20 +371,41 @@ func (n *NEO) getGASPerBlock(ic *interop.Context, _ []stackitem.Item) stackitem. return stackitem.NewBigInteger(gas) } -func (n *NEO) getGASRecordFromDAO(d dao.DAO) state.GASRecord { - var gr state.GASRecord - si := d.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock}) - _ = gr.FromBytes(si.Value) - return gr +func (n *NEO) getSortedGASRecordFromDAO(d dao.DAO) (gasRecord, error) { + grMap, err := d.GetStorageItemsWithPrefix(n.ContractID, []byte{prefixGASPerBlock}) + if err != nil { + return gasRecord{}, fmt.Errorf("failed to get gas records from storage: %w", err) + } + var ( + i int + gr = make(gasRecord, len(grMap)) + ) + for indexBytes, gasValue := range grMap { + gr[i] = gasIndexPair{ + Index: binary.BigEndian.Uint32([]byte(indexBytes)), + GASPerBlock: *bigint.FromBytes(gasValue.Value), + } + i++ + } + sort.Slice(gr, func(i, j int) bool { + return gr[i].Index < gr[j].Index + }) + return gr, nil } // GetGASPerBlock returns gas generated for block with provided index. func (n *NEO) GetGASPerBlock(d dao.DAO, index uint32) *big.Int { - var gr state.GASRecord + var ( + gr gasRecord + err error + ) if n.gasPerBlockChanged.Load().(bool) { - gr = n.getGASRecordFromDAO(d) + gr, err = n.getSortedGASRecordFromDAO(d) + if err != nil { + panic(err) + } } else { - gr = n.gasPerBlock.Load().(state.GASRecord) + gr = n.gasPerBlock.Load().(gasRecord) } for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index <= index { @@ -413,21 +440,8 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) (b if err != nil || !ok { return ok, err } - si := ic.DAO.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock}) - var gr state.GASRecord - if err := gr.FromBytes(si.Value); err != nil { - return false, err - } - if len(gr) > 0 && gr[len(gr)-1].Index == index { - gr[len(gr)-1].GASPerBlock = *gas - } else { - gr = append(gr, state.GASIndexPair{ - Index: index, - GASPerBlock: *gas, - }) - } n.gasPerBlockChanged.Store(true) - return true, ic.DAO.PutStorageItem(n.ContractID, []byte{prefixGASPerBlock}, &state.StorageItem{Value: gr.Bytes()}) + return true, n.putGASRecord(ic.DAO, index, gas) } // CalculateBonus calculates amount of gas generated for holding `value` NEO from start to end block. @@ -437,10 +451,17 @@ func (n *NEO) CalculateBonus(ic *interop.Context, value *big.Int, start, end uin } else if value.Sign() < 0 { return nil, errors.New("negative value") } - si := ic.DAO.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock}) - var gr state.GASRecord - if err := gr.FromBytes(si.Value); err != nil { - return nil, err + var ( + gr gasRecord + err error + ) + if !n.gasPerBlockChanged.Load().(bool) { + gr = n.gasPerBlock.Load().(gasRecord) + } else { + gr, err = n.getSortedGASRecordFromDAO(ic.DAO) + if err != nil { + return nil, err + } } var sum, tmp big.Int for i := len(gr) - 1; i >= 0; i-- { @@ -751,3 +772,15 @@ func toPublicKey(s stackitem.Item) *keys.PublicKey { } return pub } + +// putGASRecord is a helper which creates key and puts GASPerBlock value into the storage. +func (n *NEO) putGASRecord(dao dao.DAO, index uint32, value *big.Int) error { + key := make([]byte, 5) + key[0] = prefixGASPerBlock + binary.BigEndian.PutUint32(key[1:], index) + si := &state.StorageItem{ + Value: bigint.ToBytes(value), + IsConst: false, + } + return dao.PutStorageItem(n.ContractID, key, si) +} diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index c6cfcd1ff..408daddb5 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -2,7 +2,6 @@ package state import ( "crypto/elliptic" - "errors" "math/big" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -22,76 +21,6 @@ type NEOBalanceState struct { VoteTo *keys.PublicKey } -// GASIndexPair contains block index together with generated gas per block. -type GASIndexPair struct { - Index uint32 - GASPerBlock big.Int -} - -// GASRecord contains history of gas per block changes. -type GASRecord []GASIndexPair - -// Bytes serializes g to []byte. -func (g *GASRecord) Bytes() []byte { - w := io.NewBufBinWriter() - g.EncodeBinary(w.BinWriter) - return w.Bytes() -} - -// FromBytes deserializes g from data. -func (g *GASRecord) FromBytes(data []byte) error { - r := io.NewBinReaderFromBuf(data) - g.DecodeBinary(r) - return r.Err -} - -// DecodeBinary implements io.Serializable. -func (g *GASRecord) DecodeBinary(r *io.BinReader) { - item := stackitem.DecodeBinaryStackItem(r) - if r.Err == nil { - r.Err = g.fromStackItem(item) - } -} - -// EncodeBinary implements io.Serializable. -func (g *GASRecord) EncodeBinary(w *io.BinWriter) { - item := g.toStackItem() - stackitem.EncodeBinaryStackItem(item, w) -} - -// toStackItem converts GASRecord to a stack item. -func (g *GASRecord) toStackItem() stackitem.Item { - items := make([]stackitem.Item, len(*g)) - for i := range items { - items[i] = stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64((*g)[i].Index))), - stackitem.NewBigInteger(&(*g)[i].GASPerBlock), - }) - } - return stackitem.NewArray(items) -} - -var errInvalidFormat = errors.New("invalid item format") - -// fromStackItem converts item to a GASRecord. -func (g *GASRecord) fromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok { - return errInvalidFormat - } - for i := range arr { - s, ok := arr[i].Value().([]stackitem.Item) - if !ok || len(s) != 2 || s[0].Type() != stackitem.IntegerT || s[1].Type() != stackitem.IntegerT { - return errInvalidFormat - } - *g = append(*g, GASIndexPair{ - Index: uint32(s[0].Value().(*big.Int).Uint64()), - GASPerBlock: *s[1].Value().(*big.Int), - }) - } - return nil -} - // NEP5BalanceStateFromBytes converts serialized NEP5BalanceState to structure. func NEP5BalanceStateFromBytes(b []byte) (*NEP5BalanceState, error) { balance := new(NEP5BalanceState) diff --git a/pkg/core/state/native_state_test.go b/pkg/core/state/native_state_test.go deleted file mode 100644 index 920a7f331..000000000 --- a/pkg/core/state/native_state_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package state - -import ( - "math/big" - "testing" - - "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/stretchr/testify/require" -) - -func TestGASRecord_EncodeBinary(t *testing.T) { - expected := &GASRecord{ - GASIndexPair{ - Index: 1, - GASPerBlock: *big.NewInt(123), - }, - GASIndexPair{ - Index: 2, - GASPerBlock: *big.NewInt(7), - }, - } - testserdes.EncodeDecodeBinary(t, expected, new(GASRecord)) -} - -func TestGASRecord_fromStackItem(t *testing.T) { - t.Run("NotArray", func(t *testing.T) { - item := stackitem.Null{} - require.Error(t, new(GASRecord).fromStackItem(item)) - }) - t.Run("InvalidFormat", func(t *testing.T) { - item := stackitem.NewArray([]stackitem.Item{ - stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(1)), - stackitem.NewBool(true), - }), - }) - require.Error(t, new(GASRecord).fromStackItem(item)) - }) -}