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 15, 2024
1 parent 6a5813a commit a9edb96
Show file tree
Hide file tree
Showing 6 changed files with 136 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
44 changes: 44 additions & 0 deletions contracts/container/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand Down Expand Up @@ -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]
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":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}
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.

85 changes: 85 additions & 0 deletions tests/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = 1
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)
}

0 comments on commit a9edb96

Please sign in to comment.