Skip to content

Commit

Permalink
Support migration of the FireFly contract from one location to another
Browse files Browse the repository at this point in the history
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 <andrew.richardson@kaleido.io>
  • Loading branch information
awrichar committed May 20, 2022
1 parent 26fdac4 commit cd6d752
Show file tree
Hide file tree
Showing 31 changed files with 700 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE namespaces DROP COLUMN contract_index;
COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0;
COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces DROP COLUMN contract_index;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0;
55 changes: 55 additions & 0 deletions docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions internal/apiserver/route_post_network_migrate.go
Original file line number Diff line number Diff line change
@@ -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
},
}
43 changes: 43 additions & 0 deletions internal/apiserver/route_post_network_migrate_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions internal/apiserver/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var routes = append(
getStatusBatchManager,
getStatusPins,
getStatusWebSockets,
postNetworkMigrate,
postNewNamespace,
postNewOrganization,
postNewOrganizationSelf,
Expand Down
30 changes: 24 additions & 6 deletions internal/blockchain/ethereum/ethereum.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions internal/blockchain/ethereum/ethereum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}
31 changes: 25 additions & 6 deletions internal/blockchain/fabric/fabric.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit cd6d752

Please sign in to comment.