Skip to content

Commit

Permalink
FAB-14016 Wire new init check
Browse files Browse the repository at this point in the history
This CR causes the chaincode package to enforce the new init semantics.

It enforces that:

* If the old lifecycle is in use, normal invocations are never treated
  as init.

* If the new lifecycle is in use and the chaincod definition requires
init, normal invocations of the function named 'init' are treated as
init, and the chaincode must be initialized exactly once for each
version of the chaincode.

Change-Id: Ic838bbcdf1290e22bbcf33a0c6fd07bf9524f85e
Signed-off-by: Jason Yellick <jyellick@us.ibm.com>
  • Loading branch information
Jason Yellick committed Feb 28, 2019
1 parent 7ac72a5 commit 10fd1b2
Show file tree
Hide file tree
Showing 17 changed files with 2,066 additions and 1,095 deletions.
216 changes: 216 additions & 0 deletions core/chaincode/chaincode_ginkgo_support_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package chaincode_test

import (
"fmt"

"github.com/hyperledger/fabric/core/chaincode"
"github.com/hyperledger/fabric/core/chaincode/fake"
"github.com/hyperledger/fabric/core/chaincode/mock"
"github.com/hyperledger/fabric/core/common/ccprovider"
pb "github.com/hyperledger/fabric/protos/peer"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("ChaincodeSupport", func() {
var (
chaincodeSupport *chaincode.ChaincodeSupport

fakeApplicationConfigRetriever *fake.ApplicationConfigRetriever
fakeApplicationConfig *mock.ApplicationConfig
fakeApplicationCapabilities *mock.ApplicationCapabilities
)

BeforeEach(func() {
fakeApplicationCapabilities = &mock.ApplicationCapabilities{}
fakeApplicationCapabilities.LifecycleV20Returns(true)

fakeApplicationConfig = &mock.ApplicationConfig{}
fakeApplicationConfig.CapabilitiesReturns(fakeApplicationCapabilities)

fakeApplicationConfigRetriever = &fake.ApplicationConfigRetriever{}
fakeApplicationConfigRetriever.GetApplicationConfigReturns(fakeApplicationConfig, true)

chaincodeSupport = &chaincode.ChaincodeSupport{
AppConfig: fakeApplicationConfigRetriever,
}
})

Describe("CheckInit", func() {
var (
txParams *ccprovider.TransactionParams
cccid *ccprovider.CCContext
input *pb.ChaincodeInput

fakeSimulator *mock.TxSimulator
)

BeforeEach(func() {
fakeSimulator = &mock.TxSimulator{}
fakeSimulator.GetStateReturns([]byte("old-cc-version"), nil)

txParams = &ccprovider.TransactionParams{
ChannelID: "channel-id",
TXSimulator: fakeSimulator,
}

cccid = &ccprovider.CCContext{
Name: "cc-name",
Version: "cc-version",
InitRequired: true,
}

input = &pb.ChaincodeInput{
Args: [][]byte{[]byte("init")},
}
})

It("indicates that it is init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeTrue())

Expect(fakeSimulator.GetStateCallCount()).To(Equal(1))
namespace, key := fakeSimulator.GetStateArgsForCall(0)
Expect(namespace).To(Equal("cc-name"))
Expect(key).To(Equal("\x00\x00initialized"))

Expect(fakeSimulator.SetStateCallCount()).To(Equal(1))
namespace, key, value := fakeSimulator.SetStateArgsForCall(0)
Expect(namespace).To(Equal("cc-name"))
Expect(key).To(Equal("\x00\x00initialized"))
Expect(value).To(Equal([]byte("cc-version")))
})

Context("when the version is not changed", func() {
BeforeEach(func() {
cccid.Version = "old-cc-version"
})

It("returns an error", func() {
_, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).To(MatchError("chaincode 'cc-name' is already initialized but 'init' called"))
})

Context("when the invocation is not 'init'", func() {
BeforeEach(func() {
input.Args = [][]byte{[]byte("my-func")}
})

It("returns that it is not an init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeFalse())
Expect(fakeSimulator.GetStateCallCount()).To(Equal(1))
Expect(fakeSimulator.SetStateCallCount()).To(Equal(0))
})

Context("when the invocation contains no arguments", func() {
BeforeEach(func() {
input.Args = nil
})

It("returns that it is an init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeFalse())
})
})

})
})

Context("when init is not required", func() {
BeforeEach(func() {
cccid.InitRequired = false
})

It("returns that it is not init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeFalse())
Expect(fakeSimulator.GetStateCallCount()).To(Equal(0))
Expect(fakeSimulator.SetStateCallCount()).To(Equal(0))
})
})

Context("when the new lifecycle is not enabled", func() {
BeforeEach(func() {
fakeApplicationCapabilities.LifecycleV20Returns(false)
})

It("returns that it is not init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeFalse())
Expect(fakeSimulator.GetStateCallCount()).To(Equal(0))
Expect(fakeSimulator.SetStateCallCount()).To(Equal(0))
})
})

Context("when the invocation is channel-less", func() {
BeforeEach(func() {
txParams.ChannelID = ""
})

It("returns it is not an init", func() {
isInit, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).NotTo(HaveOccurred())
Expect(isInit).To(BeFalse())
Expect(fakeSimulator.GetStateCallCount()).To(Equal(0))
Expect(fakeSimulator.SetStateCallCount()).To(Equal(0))
})
})

Context("when the application config cannot be retrieved", func() {
BeforeEach(func() {
fakeApplicationConfigRetriever.GetApplicationConfigReturns(nil, false)
})

It("returns an error", func() {
_, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).To(MatchError("could not retrieve application config for channel 'channel-id'"))
})
})

Context("when the invocation is not 'init'", func() {
BeforeEach(func() {
input.Args = [][]byte{[]byte("my-func")}
})

It("returns an error", func() {
_, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).To(MatchError("chaincode 'cc-name' has not been initialized for this version, must call 'init' first"))
})
})

Context("when the txsimulator cannot get state", func() {
BeforeEach(func() {
fakeSimulator.GetStateReturns(nil, fmt.Errorf("get-state-error"))
})

It("wraps and returns the error", func() {
_, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).To(MatchError("could not get 'initialized' key: get-state-error"))
})
})

Context("when the txsimulator cannot set state", func() {
BeforeEach(func() {
fakeSimulator.SetStateReturns(fmt.Errorf("set-state-error"))
})

It("wraps and returns the error", func() {
_, err := chaincodeSupport.CheckInit(txParams, cccid, input)
Expect(err).To(MatchError("could not set 'initialized' key: set-state-error"))
})
})
})
})
11 changes: 11 additions & 0 deletions core/chaincode/chaincode_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package chaincode_test
import (
"testing"

"github.com/hyperledger/fabric/common/channelconfig"
commonledger "github.com/hyperledger/fabric/common/ledger"
"github.com/hyperledger/fabric/core/chaincode"
"github.com/hyperledger/fabric/core/common/privdata"
Expand Down Expand Up @@ -146,3 +147,13 @@ type applicationConfigRetriever interface {
type collectionStore interface {
privdata.CollectionStore
}

//go:generate counterfeiter -o mock/application_capabilities.go --fake-name ApplicationCapabilities . applicationCapabilities
type applicationCapabilities interface {
channelconfig.ApplicationCapabilities
}

//go:generate counterfeiter -o mock/application_config.go --fake-name ApplicationConfig . applicationConfig
type applicationConfig interface {
channelconfig.Application
}
91 changes: 78 additions & 13 deletions core/chaincode/chaincode_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
package chaincode

import (
"bytes"
"fmt"
"time"

Expand All @@ -23,6 +24,19 @@ import (
"github.com/pkg/errors"
)

const (
// InitializedKeyName is the reserved key in a chaincode's namespace which
// records the ID of the chaincode which initialized the namespace.
// In this way, we can enforce Init exactly once semantics, whenever
// the backing chaincode bytes change (but not be required to re-initialize
// the chaincode say, when endorsement policy changes).
InitializedKeyName = "\x00\x00initialized"

// InitFunctionName is the reserved name that an invoker may specify to
// trigger invocation of the chaincode init function.
InitFunctionName = "init"
)

// Runtime is used to manage chaincode runtime instances.
type Runtime interface {
Start(ccci *ccprovider.ChaincodeContainerInfo, codePackage []byte) error
Expand Down Expand Up @@ -55,7 +69,7 @@ type ChaincodeSupport struct {
Launcher Launcher
SystemCCProvider sysccprovider.SystemChaincodeProvider
Lifecycle Lifecycle
appConfig ApplicationConfigRetriever
AppConfig ApplicationConfigRetriever
HandlerMetrics *HandlerMetrics
LaunchMetrics *LaunchMetrics
DeployedCCInfoProvider ledger.DeployedChaincodeInfoProvider
Expand Down Expand Up @@ -86,7 +100,7 @@ func NewChaincodeSupport(
ACLProvider: aclProvider,
SystemCCProvider: SystemCCProvider,
Lifecycle: lifecycle,
appConfig: appConfig,
AppConfig: appConfig,
HandlerMetrics: NewHandlerMetrics(metricsProvider),
LaunchMetrics: NewLaunchMetrics(metricsProvider),
DeployedCCInfoProvider: deployedCCInfoProvider,
Expand Down Expand Up @@ -188,7 +202,7 @@ func (cs *ChaincodeSupport) HandleChaincodeStream(stream ccintf.ChaincodeStream)
UUIDGenerator: UUIDGeneratorFunc(util.GenerateUUID),
LedgerGetter: peer.Default,
DeployedCCInfoProvider: cs.DeployedCCInfoProvider,
AppConfig: cs.appConfig,
AppConfig: cs.AppConfig,
Metrics: cs.HandlerMetrics,
}

Expand Down Expand Up @@ -291,21 +305,72 @@ func (cs *ChaincodeSupport) Invoke(txParams *ccprovider.TransactionParams, cccid
return nil, err
}

// TODO add Init exactly once semantics here once new lifecycle
// is available. Enforced if the target channel is using the new lifecycle
//
// First, the function name of the chaincode to invoke should be checked. If it is
// "init", then consider this invocation to be of type pb.ChaincodeMessage_INIT,
// otherwise consider it to be of type pb.ChaincodeMessage_TRANSACTION,
//
// Secondly, A check should be made whether the chaincode has been
// inited, then, if true, only allow cctyp pb.ChaincodeMessage_TRANSACTION,
// otherwise, only allow cctype pb.ChaincodeMessage_INIT,
isInit, err := cs.CheckInit(txParams, cccid, input)
if err != nil {
return nil, err
}

cctype := pb.ChaincodeMessage_TRANSACTION
if isInit {
cctype = pb.ChaincodeMessage_INIT
}

return cs.execute(cctype, txParams, cccid, input, h)
}

func (cs *ChaincodeSupport) CheckInit(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (bool, error) {
if txParams.ChannelID == "" {
// Channel-less invocations must be for SCCs, so, we ignore them for now
return false, nil
}

ac, ok := cs.AppConfig.GetApplicationConfig(txParams.ChannelID)
if !ok {
return false, errors.Errorf("could not retrieve application config for channel '%s'", txParams.ChannelID)
}

if !ac.Capabilities().LifecycleV20() {
return false, nil
}

isInit := false

if len(input.Args) != 0 {
isInit = string(input.Args[0]) == InitFunctionName
}

if !cccid.InitRequired {
// If Init is not required, treat this as a normal invocation
// i.e. execute Invoke with 'init' as the function name
return false, nil
}

// At this point, we know we must enforce init exactly once semantics

value, err := txParams.TXSimulator.GetState(cccid.Name, InitializedKeyName)
if err != nil {
return false, errors.WithMessage(err, "could not get 'initialized' key")
}

needsInitialization := !bytes.Equal(value, []byte(cccid.Version))

switch {
case !isInit && !needsInitialization:
return false, nil
case !isInit && needsInitialization:
return false, errors.Errorf("chaincode '%s' has not been initialized for this version, must call 'init' first", cccid.Name)
case isInit && !needsInitialization:
return false, errors.Errorf("chaincode '%s' is already initialized but 'init' called", cccid.Name)
default:
// isInit && needsInitialization:
err = txParams.TXSimulator.SetState(cccid.Name, InitializedKeyName, []byte(cccid.Version))
if err != nil {
return false, errors.WithMessage(err, "could not set 'initialized' key")
}
return true, nil
}
}

// execute executes a transaction and waits for it to complete until a timeout value.
func (cs *ChaincodeSupport) execute(cctyp pb.ChaincodeMessage_Type, txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput, h *Handler) (*pb.ChaincodeMessage, error) {
input.Decorations = txParams.ProposalDecorations
Expand Down
Loading

0 comments on commit 10fd1b2

Please sign in to comment.