Make container.Delete() GAS cost more predictable #42

Merged
fyrchik merged 3 commits from fyrchik/frostfs-contract:fixed-marshal into support/v0.18 2024-09-04 19:51:17 +00:00
6 changed files with 133 additions and 3 deletions

View file

@ -1,6 +1,7 @@
package common
import (
"github.com/nspcc-dev/neo-go/pkg/interop/convert"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
@ -10,3 +11,18 @@ func SetSerialized(ctx storage.Context, key interface{}, value interface{}) {
data := std.Serialize(value)
storage.Put(ctx, key, data)
}
// ToFixedWidth64 converts x to bytes such that numbers <= math.MaxUint64
// have constant with of 9.
func ToFixedWidth64(x int) []byte {
data := convert.ToBytes(x)
if x < 0 || len(data) >= 9 {
return data
}
return append(data, make([]byte, 9-len(data))...)
}
// FromFixedWidth64 is a reverse function for ToFixedWidth64.
func FromFixedWidth64(x []byte) int {
return convert.ToInteger(x)
}

View file

@ -343,6 +343,11 @@ type DelInfo struct {
Epoch int
}
type delInfo struct {
Owner []byte
Epoch []byte
}
// DeletionInfo method returns container deletion info.
// If the container had never existed, NotFoundError is throwed.
// It can be used to check whether non-existing container was indeed deleted
@ -354,7 +359,12 @@ func DeletionInfo(containerID []byte) DelInfo {
if data == nil {
panic(NotFoundError)
}
return std.Deserialize(data).(DelInfo)
d := std.Deserialize(data).(delInfo)
return DelInfo{
Owner: d.Owner,
Epoch: common.FromFixedWidth64(d.Epoch),
}
}
// Get method returns a structure that contains a stable marshaled Container structure,
@ -631,9 +641,9 @@ func removeContainer(ctx storage.Context, id []byte, owner []byte) {
graveKey := append([]byte{graveKeyPrefix}, id...)
netmapContractAddr := storage.Get(ctx, netmapContractKey).(interop.Hash160)
epoch := contract.Call(netmapContractAddr, "epoch", contract.ReadOnly).(int)
common.SetSerialized(ctx, graveKey, DelInfo{
common.SetSerialized(ctx, graveKey, delInfo{
Owner: owner,
Epoch: epoch,
Epoch: common.ToFixedWidth64(epoch),
})
}

61
tests/common_test.go Normal file
View file

@ -0,0 +1,61 @@
package tests
import (
"math"
"math/big"
"path"
"testing"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/stretchr/testify/require"
)
const testdataPath = "./testdata"
func newTestdataInvoker(t *testing.T) *neotest.ContractInvoker {
e := newExecutor(t)
ctr := neotest.CompileFile(t, e.CommitteeHash, testdataPath, path.Join(testdataPath, "config.yml"))
e.DeployContract(t, ctr, nil)
return e.CommitteeInvoker(ctr.Hash)
}
func TestEncodeU64(t *testing.T) {
// Let's check boundary values for all bit sizes:
var nums []uint64
for i := 0; i < 64; i++ {
if i != 0 {
nums = append(nums, (1<<i)-1)
}
nums = append(nums, 1<<i)
if i != 63 {
nums = append(nums, (1<<i)+1)
}
}
c := newTestdataInvoker(t)
for _, n := range nums {
v, err := c.TestInvoke(t, "encode", n)
require.NoError(t, err)
require.Equal(t, 1, v.Len())
r := v.Pop().Bytes()
require.Equal(t, 9, len(r), "got: %x", r)
v, err = c.TestInvoke(t, "encodeDecode", n)
require.NoError(t, err)
require.Equal(t, 1, v.Len())
require.Equal(t, n, v.Pop().BigInt().Uint64())
}
t.Run("bad cases should be handled", func(t *testing.T) {
x := new(big.Int).SetUint64(math.MaxUint64)
x.Add(x, big.NewInt(1))
nums := []*big.Int{x, big.NewInt(-1), big.NewInt(-128)}
for _, n := range nums {
v, err := c.TestInvoke(t, "encodeDecode", n)
require.NoError(t, err)
require.Equal(t, 0, v.Pop().BigInt().Cmp(n))
}
})
}

View file

@ -282,6 +282,30 @@ func TestContainerDelete(t *testing.T) {
c.InvokeFail(t, container.NotFoundError, "deletionInfo", id[:])
})
t.Run("gas costs are the same for different epochs", func(t *testing.T) {
_, cnt2 := addContainer(t, c, cBal)
args := []interface{}{cnt2.id[:], cnt2.sig, cnt2.pub, cnt2.token}
tx := c.PrepareInvoke(t, "delete", args...)
for _, e := range []int{126, 127, 128, 129, 65536} {
cNm.Invoke(t, stackitem.Null{}, "newEpoch", e)
// Sanity check.
s, err := cNm.TestInvoke(t, "epoch")
require.NoError(t, err)
require.Equal(t, big.NewInt(int64(e)), s.Top().BigInt())
tx2 := c.PrepareInvoke(t, "delete", args...)
require.Equal(t, tx.Size(), tx2.Size())
require.Equal(t, tx.SystemFee, tx2.SystemFee)
require.Equal(t, tx.NetworkFee, tx2.NetworkFee)
}
// Another sanity check: we want to test successful invocations,
// bad ones can trivially be equal.
c.Invoke(t, stackitem.Null{}, "delete", args...)
})
c.InvokeFail(t, container.NotFoundError, "get", cnt.id[:])
}

2
tests/testdata/config.yml vendored Normal file
View file

@ -0,0 +1,2 @@
name: "TestContract"
safemethods: ["encodeDecode"]

17
tests/testdata/encode.go vendored Normal file
View file

@ -0,0 +1,17 @@
package testdata
import (
"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
)
// EncodeDecode encodes x in fixed-width little-endian representation
// and deserializes it back.
func EncodeDecode(x int) int {
y := common.ToFixedWidth64(x)
return common.FromFixedWidth64(y)
}
// Encode encodes x in fixed-width little-endian representation.
func Encode(x int) []byte {
return common.ToFixedWidth64(x)
}