From cd6d752825fd28854d44f0e8c10893bf54d19ce0 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 20 May 2022 16:54:28 -0400 Subject: [PATCH] Support migration of the FireFly contract from one location to another Send a specially formatted "BatchPin" transaction to signal that all members should migrate to a specific version of the contract from their config. Signed-off-by: Andrew Richardson --- ...00090_add_namespace_contractindex.down.sql | 3 + .../000090_add_namespace_contractindex.up.sql | 3 + ...00090_add_namespace_contractindex.down.sql | 1 + .../000090_add_namespace_contractindex.up.sql | 1 + docs/swagger/swagger.yaml | 55 +++++++ .../apiserver/route_post_network_migrate.go | 42 +++++ .../route_post_network_migrate_test.go | 43 +++++ internal/apiserver/routes.go | 1 + internal/blockchain/ethereum/ethereum.go | 30 +++- internal/blockchain/ethereum/ethereum_test.go | 69 ++++++++ internal/blockchain/fabric/fabric.go | 31 +++- internal/blockchain/fabric/fabric_test.go | 65 ++++++++ internal/coremsgs/en_api_translations.go | 1 + internal/coremsgs/en_struct_descriptions.go | 13 +- internal/database/sqlcommon/namespace_sql.go | 4 + .../database/sqlcommon/namespace_sql_test.go | 26 +-- internal/events/blockchain_event.go | 2 +- internal/events/event_manager.go | 1 + internal/events/operator_action.go | 58 +++++++ internal/events/operator_action_test.go | 148 ++++++++++++++++++ internal/orchestrator/bound_callbacks.go | 4 + internal/orchestrator/bound_callbacks_test.go | 4 + internal/orchestrator/orchestrator.go | 18 ++- internal/orchestrator/orchestrator_test.go | 23 +++ mocks/blockchainmocks/callbacks.go | 14 ++ mocks/blockchainmocks/plugin.go | 14 ++ mocks/eventmocks/event_manager.go | 14 ++ mocks/orchestratormocks/orchestrator.go | 14 ++ pkg/blockchain/plugin.go | 19 ++- pkg/core/namespace.go | 17 +- pkg/database/plugin.go | 6 +- 31 files changed, 700 insertions(+), 44 deletions(-) create mode 100644 db/migrations/postgres/000090_add_namespace_contractindex.down.sql create mode 100644 db/migrations/postgres/000090_add_namespace_contractindex.up.sql create mode 100644 db/migrations/sqlite/000090_add_namespace_contractindex.down.sql create mode 100644 db/migrations/sqlite/000090_add_namespace_contractindex.up.sql create mode 100644 internal/apiserver/route_post_network_migrate.go create mode 100644 internal/apiserver/route_post_network_migrate_test.go create mode 100644 internal/events/operator_action.go create mode 100644 internal/events/operator_action_test.go diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.down.sql b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql new file mode 100644 index 000000000..30a75a161 --- /dev/null +++ b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE namespaces DROP COLUMN contract_index; +COMMIT; diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.up.sql b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql new file mode 100644 index 000000000..370aae8d1 --- /dev/null +++ b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; +COMMIT; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql new file mode 100644 index 000000000..2899a529f --- /dev/null +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces DROP COLUMN contract_index; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql new file mode 100644 index 000000000..93f8a8d02 --- /dev/null +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0956bb71b..f8ed2b0ed 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8136,6 +8136,10 @@ paths: schema: items: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8206,6 +8210,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8241,6 +8249,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8300,6 +8312,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -22395,6 +22411,45 @@ paths: description: "" tags: - Global + /network/migrate: + post: + description: Instruct the network to unsubscribe from the current FireFly contract + and migrate to the next one configured + operationId: postNetworkMigrate + parameters: + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer + type: object + responses: + "202": + content: + application/json: + schema: + properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer + type: object + description: Success + default: + description: "" + tags: + - Global /network/nodes: get: description: Gets a list of nodes in the network diff --git a/internal/apiserver/route_post_network_migrate.go b/internal/apiserver/route_post_network_migrate.go new file mode 100644 index 000000000..c0c7ae556 --- /dev/null +++ b/internal/apiserver/route_post_network_migrate.go @@ -0,0 +1,42 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http" + + "github.com/hyperledger/firefly/internal/coremsgs" + "github.com/hyperledger/firefly/internal/oapispec" + "github.com/hyperledger/firefly/pkg/core" +) + +var postNetworkMigrate = &oapispec.Route{ + Name: "postNetworkMigrate", + Path: "network/migrate", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + FilterFactory: nil, + Description: coremsgs.APIEndpointsPostNetworkMigrate, + JSONInputValue: func() interface{} { return &core.NamespaceMigration{} }, + JSONOutputValue: func() interface{} { return &core.NamespaceMigration{} }, + JSONOutputCodes: []int{http.StatusAccepted}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + err = getOr(r.Ctx).MigrateNetwork(r.Ctx, r.Input.(*core.NamespaceMigration).ContractIndex) + return r.Input, err + }, +} diff --git a/internal/apiserver/route_post_network_migrate_test.go b/internal/apiserver/route_post_network_migrate_test.go new file mode 100644 index 000000000..62a64d998 --- /dev/null +++ b/internal/apiserver/route_post_network_migrate_test.go @@ -0,0 +1,43 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostNetworkMigrate(t *testing.T) { + o, r := newTestAPIServer() + input := core.NamespaceMigration{ContractIndex: 1} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/network/migrate", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + o.On("MigrateNetwork", mock.Anything, 1).Return(nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index d943792b1..f6015da71 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -38,6 +38,7 @@ var routes = append( getStatusBatchManager, getStatusPins, getStatusWebSockets, + postNetworkMigrate, postNewNamespace, postNewOrganization, postNewOrganizationSelf, diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index cd06b27f0..7fd094786 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -312,7 +312,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON logIndex := msgJSON.GetInt64("logIndex") dataJSON := msgJSON.GetObject("data") authorAddress := dataJSON.GetString("author") - ns := dataJSON.GetString("namespace") + nsOrAction := dataJSON.GetString("namespace") sUUIDs := dataJSON.GetString("uuids") sBatchHash := dataJSON.GetString("batchHash") sPayloadRef := dataJSON.GetString("payloadRef") @@ -338,6 +338,16 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON log.L(ctx).Errorf("BatchPin event is not valid - bad from address (%s): %+v", err, msgJSON) return nil // move on } + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: authorAddress, + } + + // Check if this is actually an operator action + if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { + action := nsOrAction[len(blockchain.FireFlyActionPrefix):] + return e.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) if err != nil || len(hexUUIDs) != 32 { @@ -369,7 +379,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON delete(msgJSON, "data") batch := &blockchain.BatchPin{ - Namespace: ns, + Namespace: nsOrAction, TransactionID: &txnID, BatchID: &batchID, BatchHash: &batchHash, @@ -389,10 +399,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON } // If there's an error dispatching the event, we must return the error and shutdown - return e.callbacks.BatchPinComplete(batch, &core.VerifierRef{ - Type: core.VerifierTypeEthAddress, - Value: authorAddress, - }) + return e.callbacks.BatchPinComplete(batch, verifier) } func (e *Ethereum) handleContractEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { @@ -633,6 +640,17 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) } +func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { + input := []interface{}{ + blockchain.FireFlyActionPrefix + action, + ethHexFormatB32(nil), + ethHexFormatB32(nil), + payload, + []string{}, + } + return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) +} + func (e *Ethereum) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { ethereumLocation, err := parseContractLocation(ctx, location) if err != nil { diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 4a138fea3..cccc933b7 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -2597,3 +2597,72 @@ func TestGenerateEventSignatureInvalid(t *testing.T) { signature := e.GenerateEventSignature(context.Background(), event) assert.Equal(t, "", signature) } + +func TestSubmitOperatorAction(t *testing.T) { + e, _ := newTestEthereum() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", `http://localhost:12345/`, + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + params := body["params"].([]interface{}) + headers := body["headers"].(map[string]interface{}) + assert.Equal(t, "SendTransaction", headers["type"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[1]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[2]) + assert.Equal(t, "1", params[3]) + return httpmock.NewJsonResponderOrPanic(200, "")(req) + }) + + err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", blockchain.OperatorActionMigrate, "1") + assert.NoError(t, err) +} + +func TestHandleOperatorAction(t *testing.T) { + data := fftypes.JSONAnyPtr(` +[ + { + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + "blockNumber": "38011", + "transactionIndex": "0x0", + "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", + "data": { + "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", + "namespace": "firefly:migrate", + "uuids": "0x0000000000000000000000000000000000000000000000000000000000000000", + "batchHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "payloadRef": "1", + "contexts": [] + }, + "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", + "logIndex": "50", + "timestamp": "1620576488" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Ethereum{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + } + + expectedSigningKeyRef := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x91d2b4381a4cd5c7c0f27565a7d4b829844c8635", + } + + em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + + var events []interface{} + err := json.Unmarshal(data.Bytes(), &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index ea412f6c8..6a50d65a8 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -304,12 +304,23 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb eventIndex := msgJSON.GetInt64("eventIndex") timestamp := msgJSON.GetInt64("timestamp") signer := payload.GetString("signer") - ns := payload.GetString("namespace") + nsOrAction := payload.GetString("namespace") sUUIDs := payload.GetString("uuids") sBatchHash := payload.GetString("batchHash") sPayloadRef := payload.GetString("payloadRef") sContexts := payload.GetStringArray("contexts") + verifier := &core.VerifierRef{ + Type: core.VerifierTypeMSPIdentity, + Value: signer, + } + + // Check if this is actually an operator action + if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { + action := nsOrAction[len(blockchain.FireFlyActionPrefix):] + return f.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + } + hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) if err != nil || len(hexUUIDs) != 32 { log.L(ctx).Errorf("BatchPin event is not valid - bad uuids (%s): %s", sUUIDs, err) @@ -340,7 +351,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb delete(msgJSON, "payload") batch := &blockchain.BatchPin{ - Namespace: ns, + Namespace: nsOrAction, TransactionID: &txnID, BatchID: &batchID, BatchHash: &batchHash, @@ -360,10 +371,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb } // If there's an error dispatching the event, we must return the error and shutdown - return f.callbacks.BatchPinComplete(batch, &core.VerifierRef{ - Type: core.VerifierTypeMSPIdentity, - Value: signer, - }) + return f.callbacks.BatchPinComplete(batch, verifier) } func (f *Fabric) buildEventLocationString(msgJSON fftypes.JSONObject) string { @@ -613,7 +621,18 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, "payloadRef": batch.BatchPayloadRef, "contexts": hashes, } + input, _ := jsonEncodeInput(pinInput) + return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) +} +func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { + pinInput := map[string]interface{}{ + "namespace": "firefly:" + action, + "uuids": hexFormatB32(nil), + "batchHash": hexFormatB32(nil), + "payloadRef": payload, + "contexts": []string{}, + } input, _ := jsonEncodeInput(pinInput) return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 88d07d5ae..af2d4199e 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -1766,3 +1766,68 @@ func TestGenerateEventSignature(t *testing.T) { signature := e.GenerateEventSignature(context.Background(), &core.FFIEventDefinition{Name: "Changed"}) assert.Equal(t, "Changed", signature) } + +func TestSubmitOperatorAction(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + + signer := "signer001" + + httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`, + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"]) + assert.Equal(t, "1", (body["args"].(map[string]interface{}))["payloadRef"]) + return httpmock.NewJsonResponderOrPanic(200, "")(req) + }) + + err := e.SubmitOperatorAction(context.Background(), nil, signer, "migrate", "1") + assert.NoError(t, err) + +} + +func TestHandleOperatorAction(t *testing.T) { + data := []byte(` +[ + { + "chaincodeId": "firefly", + "blockNumber": 91, + "transactionId": "ce79343000e851a0c742f63a733ce19a5f8b9ce1c719b6cecd14f01bcf81fff2", + "transactionIndex": 2, + "eventIndex": 50, + "eventName": "BatchPin", + "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoiZmlyZWZseTptaWdyYXRlIiwidXVpZHMiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJiYXRjaEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJwYXlsb2FkUmVmIjoiMSIsImNvbnRleHRzIjpbXX0=", + "subId": "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Fabric{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e", + } + + expectedSigningKeyRef := &core.VerifierRef{ + Type: core.VerifierTypeMSPIdentity, + Value: "u0vgwu9s00-x509::CN=user2,OU=client::CN=fabric-ca-server", + } + + em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + + var events []interface{} + err := json.Unmarshal(data, &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index 7826cc5f9..faba3f0e9 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -168,6 +168,7 @@ var ( APIEndpointsPutContractAPI = ffm("api.endpoints.putContractAPI", "Updates an existing contract API") APIEndpointsPutSubscription = ffm("api.endpoints.putSubscription", "Update an existing subscription") APIEndpointsGetContractAPIInterface = ffm("api.endpoints.getContractAPIInterface", "Gets a contract interface for a contract API") + APIEndpointsPostNetworkMigrate = ffm("api.endpoints.postNetworkMigrate", "Instruct the network to unsubscribe from the current FireFly contract and migrate to the next one configured") APISuccessResponse = ffm("api.success", "Success") APIRequestTimeoutDesc = ffm("api.requestTimeout", "Server-side request timeout (millseconds, or set a custom suffix like 10s)") diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 34e00a987..13568e6c9 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -371,12 +371,13 @@ var ( VerifierCreated = ffm("Verifier.created", "The time this verifier was created on this node") // Namespace field descriptions - NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") - NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") - NamespaceName = ffm("Namespace.name", "The namespace name") - NamespaceDescription = ffm("Namespace.description", "A description of the namespace") - NamespaceType = ffm("Namespace.type", "The type of the namespace") - NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") + NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") + NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") + NamespaceName = ffm("Namespace.name", "The namespace name") + NamespaceDescription = ffm("Namespace.description", "A description of the namespace") + NamespaceType = ffm("Namespace.type", "The type of the namespace") + NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") + NamespaceContractIndex = ffm("Namespace.contractIndex", "The index of the configured FireFly smart contract currently being used") // NodeStatus field descriptions NodeStatusNode = ffm("NodeStatus.node", "Details of the local node") diff --git a/internal/database/sqlcommon/namespace_sql.go b/internal/database/sqlcommon/namespace_sql.go index 18c8c086e..f7b4c0c00 100644 --- a/internal/database/sqlcommon/namespace_sql.go +++ b/internal/database/sqlcommon/namespace_sql.go @@ -37,6 +37,7 @@ var ( "name", "description", "created", + "contract_index", } namespaceFilterFieldMap = map[string]string{ "message": "message_id", @@ -90,6 +91,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa Set("name", namespace.Name). Set("description", namespace.Description). Set("created", namespace.Created). + Set("contract_index", namespace.ContractIndex). Where(sq.Eq{"name": namespace.Name}), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeUpdated, namespace.ID) @@ -112,6 +114,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa namespace.Name, namespace.Description, namespace.Created, + namespace.ContractIndex, ), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeCreated, namespace.ID) @@ -133,6 +136,7 @@ func (s *SQLCommon) namespaceResult(ctx context.Context, row *sql.Rows) (*core.N &namespace.Name, &namespace.Description, &namespace.Created, + &namespace.ContractIndex, ) if err != nil { return nil, i18n.WrapError(ctx, err, coremsgs.MsgDBReadErr, namespacesTable) diff --git a/internal/database/sqlcommon/namespace_sql_test.go b/internal/database/sqlcommon/namespace_sql_test.go index 5861f0f27..d04c0ecd4 100644 --- a/internal/database/sqlcommon/namespace_sql_test.go +++ b/internal/database/sqlcommon/namespace_sql_test.go @@ -40,11 +40,12 @@ func TestNamespacesE2EWithDB(t *testing.T) { // Create a new namespace entry namespace := &core.Namespace{ - ID: nil, // generated for us - Message: fftypes.NewUUID(), - Type: core.NamespaceTypeLocal, - Name: "namespace1", - Created: fftypes.Now(), + ID: nil, // generated for us + Message: fftypes.NewUUID(), + Type: core.NamespaceTypeLocal, + Name: "namespace1", + Created: fftypes.Now(), + ContractIndex: 1, } s.callbacks.On("UUIDCollectionEvent", database.CollectionNamespaces, core.ChangeEventTypeCreated, mock.Anything, mock.Anything).Return() @@ -239,14 +240,15 @@ func TestGetNamespaceByIDSuccess(t *testing.T) { nsID := fftypes.NewUUID() currTime := fftypes.Now() nsMock := &core.Namespace{ - ID: nsID, - Message: msgID, - Name: "ns1", - Type: core.NamespaceTypeLocal, - Description: "foo", - Created: currTime, + ID: nsID, + Message: msgID, + Name: "ns1", + Type: core.NamespaceTypeLocal, + Description: "foo", + Created: currTime, + ContractIndex: 0, } - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String())) + mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created", "contract_index"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String(), 0)) ns, err := s.GetNamespaceByID(context.Background(), nsID) assert.NoError(t, err) assert.Equal(t, nsMock, ns) diff --git a/internal/events/blockchain_event.go b/internal/events/blockchain_event.go index 62a5c52d7..561a66ba6 100644 --- a/internal/events/blockchain_event.go +++ b/internal/events/blockchain_event.go @@ -117,7 +117,7 @@ func (em *eventManager) emitBlockchainEventMetric(event *blockchain.Event) { } func (em *eventManager) BlockchainEvent(event *blockchain.EventWithSubscription) error { - return em.retry.Do(em.ctx, "persist contract event", func(attempt int) (bool, error) { + return em.retry.Do(em.ctx, "persist blockchain event", func(attempt int) (bool, error) { err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { sub, err := em.getChainListenerByProtocolIDCached(ctx, event.Subscription) if err != nil { diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index b74080fa5..e90a641a3 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -66,6 +66,7 @@ type EventManager interface { // Bound blockchain callbacks BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error BlockchainEvent(event *blockchain.EventWithSubscription) error + BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error // Bound dataexchange callbacks DXEvent(dx dataexchange.Plugin, event dataexchange.DXEvent) diff --git a/internal/events/operator_action.go b/internal/events/operator_action.go new file mode 100644 index 000000000..439429b5f --- /dev/null +++ b/internal/events/operator_action.go @@ -0,0 +1,58 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "strconv" + + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly/pkg/blockchain" + "github.com/hyperledger/firefly/pkg/core" +) + +func (em *eventManager) actionMigrate(bi blockchain.Plugin, payload string) error { + ns, err := em.database.GetNamespace(em.ctx, core.SystemNamespace) + if err != nil { + return err + } + idx, err := strconv.Atoi(payload) + if err != nil { + return err + } + if ns.ContractIndex == idx { + log.L(em.ctx).Debugf("Ignoring namespace migration for %s (already at %d)", ns.Name, ns.ContractIndex) + return nil + } + ns.ContractIndex = idx + log.L(em.ctx).Infof("Migrating namespace %s to contract index %d", ns.Name, ns.ContractIndex) + bi.Stop() + if err := bi.Start(ns.ContractIndex); err != nil { + return err + } + return em.database.UpsertNamespace(em.ctx, ns, true) +} + +func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error { + return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (bool, error) { + // TODO: verify signing identity + if action == blockchain.OperatorActionMigrate { + return true, em.actionMigrate(bi, payload) + } + log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) + return false, nil + }) +} diff --git a/internal/events/operator_action_test.go b/internal/events/operator_action_test.go new file mode 100644 index 000000000..debaba5be --- /dev/null +++ b/internal/events/operator_action_test.go @@ -0,0 +1,148 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/blockchainmocks" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestOperatorAction(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { + return ns.ContractIndex == 1 + }), true).Return(nil) + mbi.On("Stop").Return() + mbi.On("Start", 1).Return(nil) + + err := em.BlockchainOperatorAction(mbi, "migrate", "1", &core.VerifierRef{}) + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestOperatorActionUnknown(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + + err := em.BlockchainOperatorAction(mbi, "bad", "", &core.VerifierRef{}) + assert.NoError(t, err) + + mbi.AssertExpectations(t) +} + +func TestActionMigrateQueryFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(nil, fmt.Errorf("pop")) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateBadIndex(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + + err := em.actionMigrate(mbi, "!bad") + assert.Regexp(t, "Atoi", err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateSkip(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{ContractIndex: 1}, nil) + + err := em.actionMigrate(mbi, "1") + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateStartFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mbi.On("Stop").Return(nil) + mbi.On("Start", 1).Return(fmt.Errorf("pop")) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateUpsertFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { + return ns.ContractIndex == 1 + }), true).Return(fmt.Errorf("pop")) + mbi.On("Stop").Return(nil) + mbi.On("Start", 1).Return(nil) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 9b9f011fe..ca6c1fd21 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -59,6 +59,10 @@ func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKe return bc.ei.BatchPinComplete(bc.bi, batch, signingKey) } +func (bc *boundCallbacks) BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error { + return bc.ei.BlockchainOperatorAction(bc.bi, action, payload, signingKey) +} + func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) { switch event.Type() { case dataexchange.DXEventTypeTransferResult: diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index 665c5066a..1232a8320 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -57,6 +57,10 @@ func TestBoundCallbacks(t *testing.T) { err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") + mei.On("BlockchainOperatorAction", mbi, "migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) + err = bc.BlockchainOperatorAction("migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) + assert.EqualError(t, err, "pop") + mom.On("SubmitOperationUpdate", mock.Anything, &operations.OperationUpdate{ ID: opID, Status: core.OpStatusFailed, diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 960cd87d2..8d6830738 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -18,6 +18,7 @@ package orchestrator import ( "context" + "strconv" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -140,6 +141,9 @@ type Orchestrator interface { // Message Routing RequestReply(ctx context.Context, ns string, msg *core.MessageInOut) (reply *core.MessageInOut, err error) + + // Network Operations + MigrateNetwork(ctx context.Context, contractIndex int) error } type orchestrator struct { @@ -221,9 +225,13 @@ func (or *orchestrator) Start() (err error) { if err == nil { err = or.batch.Start() } + var ns *core.Namespace + if err == nil { + ns, err = or.database.GetNamespace(or.ctx, core.SystemNamespace) + } if err == nil { for _, el := range or.blockchains { - if err = el.Start(0); err != nil { + if err = el.Start(ns.ContractIndex); err != nil { break } } @@ -866,3 +874,11 @@ func (or *orchestrator) initNamespaces(ctx context.Context) (err error) { } return or.namespace.Init(ctx, or.database) } + +func (or *orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { + verifier, err := or.identity.GetNodeOwnerBlockchainKey(ctx) + if err != nil { + return err + } + return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, blockchain.OperatorActionMigrate, strconv.Itoa(contractIndex)) +} diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 2c64d6808..d196ef769 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -978,6 +978,8 @@ func TestStartTokensFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -994,6 +996,8 @@ func TestStartBlockchainsFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(fmt.Errorf("pop")) or.mba.On("Start").Return(nil) err := or.Start() @@ -1004,6 +1008,8 @@ func TestStartStopOk(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -1111,3 +1117,20 @@ func TestInitDataExchangeWithNodes(t *testing.T) { err := or.initDataExchange(or.ctx) assert.NoError(t, err) } + +func TestMigrateNetwork(t *testing.T) { + or := newTestOrchestrator() + or.blockchain = or.mbi + verifier := &core.VerifierRef{Value: "0x123"} + or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) + or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "migrate", "1").Return(nil) + err := or.MigrateNetwork(context.Background(), 1) + assert.NoError(t, err) +} + +func TestMigrateNetworkBadKey(t *testing.T) { + or := newTestOrchestrator() + or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(nil, fmt.Errorf("pop")) + err := or.MigrateNetwork(context.Background(), 1) + assert.EqualError(t, err, "pop") +} diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go index 5144272be..de7b9830b 100644 --- a/mocks/blockchainmocks/callbacks.go +++ b/mocks/blockchainmocks/callbacks.go @@ -48,3 +48,17 @@ func (_m *Callbacks) BlockchainEvent(event *blockchain.EventWithSubscription) er func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, operationID *fftypes.UUID, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) { _m.Called(plugin, operationID, txState, blockchainTXID, errorMessage, opOutput) } + +// BlockchainOperatorAction provides a mock function with given fields: action, payload, signingKey +func (_m *Callbacks) BlockchainOperatorAction(action string, payload string, signingKey *core.VerifierRef) error { + ret := _m.Called(action, payload, signingKey) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, *core.VerifierRef) error); ok { + r0 = rf(action, payload, signingKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index d0c1ab56b..765d2679f 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -273,6 +273,20 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return r0 } +// SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action, payload +func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action string, payload string) error { + ret := _m.Called(ctx, operationID, signingKey, action, payload) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, string, string) error); ok { + r0 = rf(ctx, operationID, signingKey, action, payload) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // VerifierType provides a mock function with given fields: func (_m *Plugin) VerifierType() core.FFEnum { ret := _m.Called() diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go index 04df8e4a9..3d801ff83 100644 --- a/mocks/eventmocks/event_manager.go +++ b/mocks/eventmocks/event_manager.go @@ -69,6 +69,20 @@ func (_m *EventManager) BlockchainEvent(event *blockchain.EventWithSubscription) return r0 } +// BlockchainOperatorAction provides a mock function with given fields: bi, action, payload, signingKey +func (_m *EventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, payload string, signingKey *core.VerifierRef) error { + ret := _m.Called(bi, action, payload, signingKey) + + var r0 error + if rf, ok := ret.Get(0).(func(blockchain.Plugin, string, string, *core.VerifierRef) error); ok { + r0 = rf(bi, action, payload, signingKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateUpdateDurableSubscription provides a mock function with given fields: ctx, subDef, mustNew func (_m *EventManager) CreateUpdateDurableSubscription(ctx context.Context, subDef *core.Subscription, mustNew bool) error { ret := _m.Called(ctx, subDef, mustNew) diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index 1a5e68d3e..3710b5928 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1262,6 +1262,20 @@ func (_m *Orchestrator) Metrics() metrics.Manager { return r0 } +// MigrateNetwork provides a mock function with given fields: ctx, contractIndex +func (_m *Orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { + ret := _m.Called(ctx, contractIndex) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, contractIndex) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NetworkMap provides a mock function with given fields: func (_m *Orchestrator) NetworkMap() networkmap.Manager { ret := _m.Called() diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index b8fd02bea..317be26eb 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -55,6 +55,9 @@ type Plugin interface { // SubmitBatchPin sequences a batch of message globally to all viewers of a given ledger SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *BatchPin) error + // SubmitOperatorAction writes a special "BatchPin" event which signals the plugin to take an action + SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error + // InvokeContract submits a new transaction to be executed by custom on-chain logic InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error @@ -80,6 +83,13 @@ type Plugin interface { GenerateEventSignature(ctx context.Context, event *core.FFIEventDefinition) string } +const ( + // OperatorActionMigrate request all network members to stop using the current contract and move to the next one configured + OperatorActionMigrate = "migrate" +) + +const FireFlyActionPrefix = "firefly:" + // Callbacks is the interface provided to the blockchain plugin, to allow it to pass events back to firefly. // // Events must be delivered sequentially, such that event 2 is not delivered until the callback invoked for event 1 @@ -91,16 +101,19 @@ type Callbacks interface { // opOutput can be used to add opaque protocol specific JSON from the plugin (protocol transaction ID etc.) // Note this is an optional hook information, and stored separately to the confirmation of the actual event that was being submitted/sequenced. // Only the party submitting the transaction will see this data. - // - // Error should will only be returned in shutdown scenarios BlockchainOpUpdate(plugin Plugin, operationID *fftypes.UUID, txState TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) // BatchPinComplete notifies on the arrival of a sequenced batch of messages, which might have been // submitted by us, or by any other authorized party in the network. // - // Error should will only be returned in shutdown scenarios + // Error should only be returned in shutdown scenarios BatchPinComplete(batch *BatchPin, signingKey *core.VerifierRef) error + // BlockchainOperatorAction notifies on the arrival of a network operator action + // + // Error should only be returned in shutdown scenarios + BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error + // BlockchainEvent notifies on the arrival of any event from a user-created subscription. BlockchainEvent(event *EventWithSubscription) error } diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go index 2c2cee297..b004822c1 100644 --- a/pkg/core/namespace.go +++ b/pkg/core/namespace.go @@ -39,12 +39,17 @@ var ( // Namespace is a isolate set of named resources, to allow multiple applications to co-exist in the same network, with the same named objects. // Can be used for use case segregation, or multi-tenancy. type Namespace struct { - ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` - Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` - Name string `ffstruct:"Namespace" json:"name"` - Description string `ffstruct:"Namespace" json:"description"` - Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` - Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` + ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` + Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` + Name string `ffstruct:"Namespace" json:"name"` + Description string `ffstruct:"Namespace" json:"description"` + Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` + Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` + ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ffexcludeinput:"true"` +} + +type NamespaceMigration struct { + ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ` } func (ns *Namespace) Validate(ctx context.Context, existing bool) (err error) { diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index f8c45aca8..0316cdb58 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -67,13 +67,13 @@ type iNamespaceCollection interface { DeleteNamespace(ctx context.Context, id *fftypes.UUID) (err error) // GetNamespace - Get an namespace by name - GetNamespace(ctx context.Context, name string) (offset *core.Namespace, err error) + GetNamespace(ctx context.Context, name string) (namespace *core.Namespace, err error) // GetNamespaceByID - Get a namespace by ID - GetNamespaceByID(ctx context.Context, id *fftypes.UUID) (offset *core.Namespace, err error) + GetNamespaceByID(ctx context.Context, id *fftypes.UUID) (namespace *core.Namespace, err error) // GetNamespaces - Get namespaces - GetNamespaces(ctx context.Context, filter Filter) (offset []*core.Namespace, res *FilterResult, err error) + GetNamespaces(ctx context.Context, filter Filter) (namespaces []*core.Namespace, res *FilterResult, err error) } type iMessageCollection interface {