Skip to content

Commit

Permalink
container: add container placement verification
Browse files Browse the repository at this point in the history
The new `VerifyPlacementSignatures` checks if a message was signed by a suitable
number of nodes. Number is taken from the contract storage, their relevance
should be ensured and maintained by the Alphabet nodes every epoch.
Closes #413.

Signed-off-by: Pavel Karpy <[email protected]>
  • Loading branch information
carpawell committed Oct 16, 2024
1 parent 6a5813a commit d5e22de
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 2 deletions.
2 changes: 1 addition & 1 deletion contracts/container/config.yml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
42 changes: 42 additions & 0 deletions contracts/container/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,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) bool {
sigsLen := len(sigs)
var i int
repsI := ReplicasNumbers(cid)
repsLoop:
for iterator.Next(repsI) {
if sigsLen == i {
return false
}

m := iterator.Value(repsI).(int)
if len(sigs[i]) < m {
return false
}

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, crypto.Secp256r1Sha256) {
counter++
break
}
}

if counter == m {
i++
continue repsLoop
}
}

return false
}

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]
Expand Down
Binary file modified contracts/container/contract.nef
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/container/manifest.json
Original file line number Diff line number Diff line change
@@ -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}
{"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":4758,"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":5713,"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":5973,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":6346,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":6248,"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":6087,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":6398,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"nodes","offset":5327,"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":5771,"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":5229,"parameters":[{"name":"cID","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"setEACL","offset":5451,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":6428,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":6509,"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":4592,"parameters":[{"name":"cid","type":"Hash256"},{"name":"msg","type":"ByteArray"},{"name":"sigs","type":"Array"}],"returntype":"Boolean","safe":true},{"name":"version","offset":6589,"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}
5 changes: 5 additions & 0 deletions rpc/container/rpcbinding.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions tests/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ 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/neotest"
"github.com/nspcc-dev/neo-go/pkg/util"
Expand Down Expand Up @@ -712,3 +714,91 @@ 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)
require.NoError(t, err)
stackWithBool(t, stack, true)

// "right" signatures not in the first place
slices.Reverse(sigs[numberOfVectors-1].([]any))

stack, err = c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs)
require.NoError(t, err)
stackWithBool(t, stack, true)
})

t.Run("not enough vectors", func(t *testing.T) {
stack, err := c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs[:len(vectors)-1])
require.NoError(t, err)
stackWithBool(t, stack, false)
})

t.Run("incorrect signature", func(t *testing.T) {
k, err := keys.NewPrivateKey()
require.NoError(t, err)

const problemVector = 1
sigs[problemVector].([]any)[replicas[problemVector]-1] = k.Sign(testData)

stack, err := c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs)
require.NoError(t, err)
stackWithBool(t, stack, false)
})

t.Run("not enough signatures", func(t *testing.T) {
const problemVector = 1
sigs[problemVector] = sigs[problemVector].([]any)[:replicas[problemVector]-1]

stack, err := c.TestInvoke(t, "verifyPlacementSignatures", cID, testData, sigs)
require.NoError(t, err)
stackWithBool(t, stack, false)
})
}

func stackWithBool(t *testing.T, stack *vm.Stack, v bool) {
require.Equal(t, 1, stack.Len())

res, ok := stack.Pop().Value().(bool)
require.True(t, ok)
require.Equal(t, v, res)
}

0 comments on commit d5e22de

Please sign in to comment.