diff --git a/contracts/container/config.yml b/contracts/container/config.yml index 95c62cd8..4cd139a9 100644 --- a/contracts/container/config.yml +++ b/contracts/container/config.yml @@ -1,5 +1,5 @@ name: "NeoFS Container" -safemethods: ["alias", "count", "containersOf", "get", "owner", "list", "nodes", "replicasNumbers", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"] +safemethods: ["alias", "count", "containersOf", "get", "owner", "list", "nodes", "replicasNumbers", "verifyPlacementSignatures", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"] permissions: - methods: ["update", "addKey", "transferX", "register", "registerTLD", "addRecord", "deleteRecords", "subscribeForNewEpoch"] diff --git a/contracts/container/contract.go b/contracts/container/contract.go index 64b58435..ebf2d255 100644 --- a/contracts/container/contract.go +++ b/contracts/container/contract.go @@ -505,6 +505,8 @@ const maxNumOfREPs = 255 // only when a container list is changed, otherwise nothing should be done. // Call must be signed by the Alphabet nodes. func AddNextEpochNodes(cID interop.Hash256, placementVector uint8, publicKeys []interop.PublicKey) { + runtime.Log("fuck") + if len(cID) != interop.Hash256Len { panic(cst.ErrorInvalidContainerID + ": length: " + std.Itoa10(len(cID))) } @@ -582,6 +584,48 @@ func validatePlacementIndex(ctx storage.Context, cID interop.Hash256, inx uint8) } } +// VerifyPlacementSignatures verifies that message has been signed by container +// members according to container's placement policy: there should be at least +// REP number of signatures for every placement vector. sigs must be container's +// number of SELECTs length. +func VerifyPlacementSignatures(cid interop.Hash256, msg []byte, sigs [][]interop.Signature, curveHash crypto.NamedCurveHash) bool { + sigsLen := len(sigs) + var i int + repsI := ReplicasNumbers(cid) +repsLoop: + for iterator.Next(repsI) { + if sigsLen == i { + panic("not found signatures for " + std.Itoa10(i) + " placement vector") + } + + m := iterator.Value(repsI).(int) + if len(sigs[i]) < m { + panic("not enough signatures for " + std.Itoa10(i) + " placement vector, required: " + std.Itoa10(m)) + } + + var counter int + for _, sig := range sigs[i] { + pubsI := Nodes(cid, uint8(i)) + for iterator.Next(pubsI) { + pub := iterator.Value(pubsI).(interop.PublicKey) + if crypto.VerifyWithECDsa(msg, pub, sig, curveHash) { + counter++ + break + } + } + + if counter == m { + i++ + continue repsLoop + } + } + + panic("verified " + std.Itoa10(counter) + " signatures for " + std.Itoa10(i) + " placement vector, required: " + std.Itoa10(m)) + } + + return true +} + // CommitContainerListUpdate commits container list changes made by // [AddNextEpochNodes] calls in advance. Replicas must correspond to // ordered placement policy (REP clauses). If no [AddNextEpochNodes] diff --git a/contracts/container/contract.nef b/contracts/container/contract.nef index aee36071..e8e6b18c 100755 Binary files a/contracts/container/contract.nef and b/contracts/container/contract.nef differ diff --git a/contracts/container/manifest.json b/contracts/container/manifest.json index a87775d5..5d518178 100755 --- a/contracts/container/manifest.json +++ b/contracts/container/manifest.json @@ -1 +1 @@ -{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":83,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addNextEpochNodes","offset":3987,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"},{"name":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":3697,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":4592,"parameters":[{"name":"cID","type":"Hash256"},{"name":"replicas","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"containersOf","offset":3837,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":3792,"parameters":[],"returntype":"Integer","safe":true},{"name":"delete","offset":3187,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":5547,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":3584,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerSize","offset":5807,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":6180,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":6082,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"list","offset":3891,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listContainerSizes","offset":5921,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":6232,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":5161,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"onNEP11Payment","offset":1646,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":3646,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":2037,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putContainerSize","offset":5605,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"usedSize","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":2053,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"replicasNumbers","offset":5063,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":5285,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":6262,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":6343,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"update","offset":1904,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":6423,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"StartEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"StopEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"NodesUpdate","parameters":[{"name":"ContainerID","type":"Hash256"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","subscribeForNewEpoch"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file +{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":83,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addNextEpochNodes","offset":3987,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"},{"name":"publicKeys","type":"Array"}],"returntype":"Void","safe":false},{"name":"alias","offset":3697,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"commitContainerListUpdate","offset":4964,"parameters":[{"name":"cID","type":"Hash256"},{"name":"replicas","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"containersOf","offset":3837,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":3792,"parameters":[],"returntype":"Integer","safe":true},{"name":"delete","offset":3187,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":5919,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":3584,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerSize","offset":6179,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":6552,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":6454,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"list","offset":3891,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listContainerSizes","offset":6293,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":6604,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":5533,"parameters":[{"name":"cID","type":"Hash256"},{"name":"placementVector","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"onNEP11Payment","offset":1646,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":3646,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":2037,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putContainerSize","offset":5977,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"usedSize","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":2053,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"replicasNumbers","offset":5435,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":5657,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":6634,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":6715,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"update","offset":1904,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"verifyPlacementSignatures","offset":4603,"parameters":[{"name":"cid","type":"Hash256"},{"name":"msg","type":"ByteArray"},{"name":"sigs","type":"Array"},{"name":"curveHash","type":"InteropInterface"}],"returntype":"Boolean","safe":true},{"name":"version","offset":6795,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"StartEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"StopEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"NodesUpdate","parameters":[{"name":"ContainerID","type":"Hash256"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords","subscribeForNewEpoch"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/rpc/container/rpcbinding.go b/rpc/container/rpcbinding.go index 4363ed91..80ac4723 100644 --- a/rpc/container/rpcbinding.go +++ b/rpc/container/rpcbinding.go @@ -237,6 +237,11 @@ func (c *ContractReader) ReplicasNumbersExpanded(cID util.Uint256, _numOfIterato return unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "replicasNumbers", _numOfIteratorItems, cID)) } +// VerifyPlacementSignatures invokes `verifyPlacementSignatures` method of contract. +func (c *ContractReader) VerifyPlacementSignatures(cid util.Uint256, msg []byte, sigs [][][]byte, curveHash any) (bool, error) { + return unwrap.Bool(c.invoker.Call(c.hash, "verifyPlacementSignatures", cid, msg, sigs, curveHash)) +} + // Version invokes `version` method of contract. func (c *ContractReader) Version() (*big.Int, error) { return unwrap.BigInt(c.invoker.Call(c.hash, "version")) diff --git a/tests/container_test.go b/tests/container_test.go index 7b9c0d74..f5a9f3e1 100644 --- a/tests/container_test.go +++ b/tests/container_test.go @@ -7,11 +7,14 @@ import ( "fmt" "math/big" "path" + "slices" "testing" "github.com/mr-tron/base58" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -712,3 +715,85 @@ func stackIteratorToUint8Array(t *testing.T, stack *vm.Stack) []uint8 { return res } + +func TestVerifyPlacementSignature(t *testing.T) { + testData := make([]byte, 1024) + _, err := rand.Read(testData) + require.NoError(t, err) + cID := make([]byte, sha256.Size) + _, err = rand.Read(cID) + require.NoError(t, err) + + c, _, _ := newContainerInvoker(t, false) + + const nodesNumber = 1024 + const numberOfVectors = 4 + vectors := make([][]any, numberOfVectors) + vectorsSigners := make([][]*keys.PrivateKey, numberOfVectors) + for i := range nodesNumber { + vecNum := i / (nodesNumber / numberOfVectors) + s := c.NewAccount(t).(neotest.SingleSigner) + + vectors[vecNum] = append(vectors[vecNum], s.Account().PublicKey().Bytes()) + vectorsSigners[vecNum] = append(vectorsSigners[vecNum], s.Account().PrivateKey()) + } + var replicas []uint8 + for i := range numberOfVectors { + replicas = append(replicas, uint8(i+1)) + c.Invoke(t, stackitem.Null{}, "addNextEpochNodes", cID, i, vectors[i]) + } + c.Invoke(t, stackitem.Null{}, "commitContainerListUpdate", cID, replicas) + + sigs := make([]any, numberOfVectors) + for i, rep := range replicas { + // neo-go client's "generic" magic for args + var vectorSigs []any + for j := range rep { + vectorSigs = append(vectorSigs, vectorsSigners[i][j].Sign(testData)) + } + + sigs[i] = vectorSigs + } + + t.Run("happy path", func(t *testing.T) { + stack, err := c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs, int(crypto.Secp256r1Sha256)) + require.NoError(t, err) + stackWithTrue(t, stack) + + // "right" signatures not in the first place + slices.Reverse(sigs[numberOfVectors-1].([]any)) + + stack, err = c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs, int(crypto.Secp256r1Sha256)) + require.NoError(t, err) + stackWithTrue(t, stack) + }) + + t.Run("incorrect signature", func(t *testing.T) { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + const problemVector = 2 + sigs[problemVector].([]any)[replicas[problemVector]-1] = k.Sign(testData) + + c.InvokeFail(t, fmt.Sprintf("verified %d signatures for %d placement vector, required: %d", replicas[problemVector]-1, problemVector, replicas[problemVector]), "verifyPlacementSignatures", cID, testData, sigs, int(crypto.Secp256r1Sha256)) + }) + + t.Run("not enough signatures", func(t *testing.T) { + const problemVector = 1 + sigs[problemVector] = sigs[problemVector].([]any)[:replicas[problemVector]-1] + + c.InvokeFail(t, fmt.Sprintf("not enough signatures for %d placement vector, required: %d", problemVector, replicas[problemVector]), "verifyPlacementSignatures", cID, testData, sigs, int(crypto.Secp256r1Sha256)) + }) + + t.Run("not enough vectors", func(t *testing.T) { + c.InvokeFail(t, fmt.Sprintf("not found signatures for %d placement vector", len(sigs)-1), "verifyPlacementSignatures", cID, testData, sigs[:len(vectors)-1], int(crypto.Secp256r1Sha256)) + }) +} + +func stackWithTrue(t *testing.T, stack *vm.Stack) { + require.Equal(t, 1, stack.Len()) + + res, ok := stack.Pop().Value().(bool) + require.True(t, ok) + require.True(t, res) +}