diff --git a/container/container_contract.go b/container/container_contract.go index e86d11f..1a9ff96 100644 --- a/container/container_contract.go +++ b/container/container_contract.go @@ -61,7 +61,8 @@ const ( estimateKeyPrefix = "cnr" estimatePostfixSize = 10 - cleanupDelta = 3 + // CleanupDelta contains number last epochs for which container estimations are present. + CleanupDelta = 3 // NotFoundError is returned if container is missing. NotFoundError = "container does not exist" @@ -780,7 +781,7 @@ func cleanupContainers(ctx storage.Context, epoch int) { var n interface{} = nbytes - if epoch-n.(int) > cleanupDelta { + if epoch-n.(int) > CleanupDelta { storage.Delete(ctx, k) } } diff --git a/tests/container_test.go b/tests/container_test.go index 5628800..15f5b71 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -1,6 +1,7 @@ package tests import ( + "bytes" "crypto/sha256" "path" "testing" @@ -13,6 +14,7 @@ import ( "github.com/nspcc-dev/neofs-contract/common" "github.com/nspcc-dev/neofs-contract/container" "github.com/nspcc-dev/neofs-contract/nns" + "github.com/stretchr/testify/require" ) const containerPath = "../container" @@ -36,7 +38,7 @@ func deployContainerContract(t *testing.T, e *neotest.Executor, addrNetmap, addr return c.Hash } -func newContainerInvoker(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker) { +func newContainerInvoker(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker, *neotest.ContractInvoker) { e := newExecutor(t) ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) @@ -50,7 +52,7 @@ func newContainerInvoker(t *testing.T) (*neotest.ContractInvoker, *neotest.Contr container.AliasFeeKey, int64(containerAliasFee)) deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash) deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash) - return e.CommitteeInvoker(ctrContainer.Hash), e.CommitteeInvoker(ctrBalance.Hash) + return e.CommitteeInvoker(ctrContainer.Hash), e.CommitteeInvoker(ctrBalance.Hash), e.CommitteeInvoker(ctrNetmap.Hash) } func setContainerOwner(c []byte, acc neotest.Signer) { @@ -78,7 +80,7 @@ func dummyContainer(owner neotest.Signer) testContainer { } func TestContainerPut(t *testing.T) { - c, cBal := newContainerInvoker(t) + c, cBal, _ := newContainerInvoker(t) acc := c.NewAccount(t) cnt := dummyContainer(acc) @@ -155,7 +157,7 @@ func addContainer(t *testing.T, c, cBal *neotest.ContractInvoker) (neotest.Signe } func TestContainerDelete(t *testing.T) { - c, cBal := newContainerInvoker(t) + c, cBal, _ := newContainerInvoker(t) acc, cnt := addContainer(t, c, cBal) cAcc := c.WithSigners(acc) @@ -174,7 +176,7 @@ func TestContainerDelete(t *testing.T) { } func TestContainerOwner(t *testing.T) { - c, cBal := newContainerInvoker(t) + c, cBal, _ := newContainerInvoker(t) acc, cnt := addContainer(t, c, cBal) @@ -189,7 +191,7 @@ func TestContainerOwner(t *testing.T) { } func TestContainerGet(t *testing.T) { - c, cBal := newContainerInvoker(t) + c, cBal, _ := newContainerInvoker(t) _, cnt := addContainer(t, c, cBal) @@ -227,7 +229,7 @@ func dummyEACL(containerID [32]byte) eacl { } func TestContainerSetEACL(t *testing.T) { - c, cBal := newContainerInvoker(t) + c, cBal, _ := newContainerInvoker(t) acc, cnt := addContainer(t, c, cBal) @@ -253,3 +255,111 @@ func TestContainerSetEACL(t *testing.T) { }) c.Invoke(t, expected, "eACL", cnt.id[:]) } + +func TestContainerSizeEstimation(t *testing.T) { + c, cBal, cNm := newContainerInvoker(t) + + _, cnt := addContainer(t, c, cBal) + nodes := []testNodeInfo{ + newStorageNode(t, c), + newStorageNode(t, c), + newStorageNode(t, c), + } + for i := range nodes { + cNm.WithSigners(nodes[i].signer).Invoke(t, stackitem.Null{}, "addPeer", nodes[i].raw) + cNm.Invoke(t, stackitem.Null{}, "register", nodes[i].raw) + } + + // putContainerSize retrieves storage nodes from the previous snapshot, + // so epoch must be incremented twice. + cNm.Invoke(t, stackitem.Null{}, "newEpoch", int64(1)) + cNm.Invoke(t, stackitem.Null{}, "newEpoch", int64(2)) + + t.Run("must be witnessed by key in the argument", func(t *testing.T) { + c.WithSigners(nodes[1].signer).InvokeFail(t, common.ErrWitnessFailed, "putContainerSize", + int64(2), cnt.id[:], int64(123), nodes[0].pub) + }) + + c.WithSigners(nodes[0].signer).Invoke(t, stackitem.Null{}, "putContainerSize", + int64(2), cnt.id[:], int64(123), nodes[0].pub) + estimations := []estimation{{nodes[0].pub, 123}} + checkEstimations(t, c, 2, cnt, estimations...) + + c.WithSigners(nodes[1].signer).Invoke(t, stackitem.Null{}, "putContainerSize", + int64(2), cnt.id[:], int64(42), nodes[1].pub) + estimations = append(estimations, estimation{nodes[1].pub, int64(42)}) + checkEstimations(t, c, 2, cnt, estimations...) + + t.Run("add estimation for a different epoch", func(t *testing.T) { + c.WithSigners(nodes[2].signer).Invoke(t, stackitem.Null{}, "putContainerSize", + int64(1), cnt.id[:], int64(777), nodes[2].pub) + checkEstimations(t, c, 1, cnt, estimation{nodes[2].pub, 777}) + checkEstimations(t, c, 2, cnt, estimations...) + }) + + c.WithSigners(nodes[2].signer).Invoke(t, stackitem.Null{}, "putContainerSize", + int64(3), cnt.id[:], int64(888), nodes[2].pub) + checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888}) + + // Remove old estimations. + for i := int64(1); i <= container.CleanupDelta; i++ { + cNm.Invoke(t, stackitem.Null{}, "newEpoch", 2+i) + checkEstimations(t, c, 2, cnt, estimations...) + checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888}) + } + + cNm.Invoke(t, stackitem.Null{}, "newEpoch", int64(2+container.CleanupDelta+1)) + checkEstimations(t, c, 2, cnt) + checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888}) +} + +type estimation struct { + from []byte + size int64 +} + +func checkEstimations(t *testing.T, c *neotest.ContractInvoker, epoch int64, cnt testContainer, estimations ...estimation) { + s, err := c.TestInvoke(t, "listContainerSizes", epoch) + require.NoError(t, err) + + var id []byte + + // When there are no estimations, listContainerSizes can also return nothing. + item := s.Top().Item() + switch it := item.(type) { + case stackitem.Null: + require.Equal(t, 0, len(estimations)) + require.Equal(t, stackitem.Null{}, it) + return + case *stackitem.Array: + id, err = it.Value().([]stackitem.Item)[0].TryBytes() + require.NoError(t, err) + default: + require.FailNow(t, "invalid return type for listContainerSizes") + } + + s, err = c.TestInvoke(t, "getContainerSize", id) + require.NoError(t, err) + + sizes := s.Top().Array() + require.Equal(t, cnt.id[:], sizes[0].Value()) + + actual := sizes[1].Value().([]stackitem.Item) + require.Equal(t, len(estimations), len(actual)) + for i := range actual { + // type estimation struct { + // from interop.PublicKey + // size int + // } + est := actual[i].Value().([]stackitem.Item) + pub := est[0].Value().([]byte) + found := false + for i := range estimations { + if found = bytes.Equal(estimations[i].from, pub); found { + require.Equal(t, stackitem.Make(estimations[i].size), est[1]) + break + } + } + require.True(t, found, "expected estimation from %x to be present", pub) + } +} diff --git a/tests/netmap_test.go b/tests/netmap_test.go index 571eefa..8c6483c 100644 --- a/tests/netmap_test.go +++ b/tests/netmap_test.go @@ -56,13 +56,28 @@ func TestDeploySetConfig(t *testing.T) { "config", container.AliasFeeKey) } -func dummyNodeInfo(acc neotest.Signer) []byte { +type testNodeInfo struct { + signer neotest.SingleSigner + pub []byte + raw []byte +} + +func dummyNodeInfo(acc neotest.Signer) testNodeInfo { ni := make([]byte, 66) rand.Read(ni) - pub, _ := vm.ParseSignatureContract(acc.Script()) + s := acc.(neotest.SingleSigner) + pub := s.Account().PrivateKey().PublicKey().Bytes() copy(ni[2:], pub) - return ni + return testNodeInfo{ + signer: s, + pub: pub, + raw: ni, + } +} + +func newStorageNode(t *testing.T, c *neotest.ContractInvoker) testNodeInfo { + return dummyNodeInfo(c.NewAccount(t)) } func TestAddPeer(t *testing.T) { @@ -74,25 +89,25 @@ func TestAddPeer(t *testing.T) { acc1 := c.NewAccount(t) cAcc1 := c.WithSigners(acc1) - cAcc1.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo) + cAcc1.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo.raw) - h := cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo) + h := cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo.raw) aer := cAcc.CheckHalt(t, h) require.Equal(t, 1, len(aer.Events)) require.Equal(t, "AddPeer", aer.Events[0].Name) - require.Equal(t, stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(dummyInfo)}), + require.Equal(t, stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(dummyInfo.raw)}), aer.Events[0].Item) - dummyInfo[0] ^= 0xFF - h = cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo) + dummyInfo.raw[0] ^= 0xFF + h = cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo.raw) aer = cAcc.CheckHalt(t, h) require.Equal(t, 1, len(aer.Events)) require.Equal(t, "AddPeer", aer.Events[0].Name) - require.Equal(t, stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(dummyInfo)}), + require.Equal(t, stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(dummyInfo.raw)}), aer.Events[0].Item) - c.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo) - c.Invoke(t, stackitem.Null{}, "register", dummyInfo) + c.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo.raw) + c.Invoke(t, stackitem.Null{}, "register", dummyInfo.raw) } func TestUpdateState(t *testing.T) { @@ -103,7 +118,7 @@ func TestUpdateState(t *testing.T) { cBoth := e.WithSigners(e.Committee, acc) dummyInfo := dummyNodeInfo(acc) - cBoth.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo) + cBoth.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo.raw) pub, ok := vm.ParseSignatureContract(acc.Script()) require.True(t, ok)