Skip to content

Commit c3a3e2f

Browse files
author
Srinivasan Muralidharan
committed
FAB-23 user given name instead of fabric generated hash
FAB-23 is implemented by this changeset. Deployment usage change : name has to be specified in addition to path. For example in the sample below, "-n mycc" will serve as the name of the chaincode. peer chaincode deploy -n mycc -p github.com/hyperledger/fabric/ examples/chaincode/go/chaincode_example02 -c '{"Args":["init","a", "100","b","200"]}' The name should be unique. The Life Cycle System Chaincode (LCCC) maps unique chaincode names to other chaincode properies such as Version and deployment spec using chaincode state variables. With these we no longer need to depend upon generated hash to provide uniqueness. We may still use hash to restrict duplicate deployments of the same code but using different names in a separate story. Also we will disallow some characters in chaincode name for use in future support for namespaces. Currently these characters are "/[]{}$:". Unit tests have been modified to use name. In particular, example04 no longer needs to depend on generated hash and uses user provided name. Change-Id: I6b81f75e54cb13aa19b9c9d982b3756c5dca7440 Signed-off-by: Srinivasan Muralidharan <[email protected]>
1 parent 351423d commit c3a3e2f

File tree

17 files changed

+120
-76
lines changed

17 files changed

+120
-76
lines changed

core/chaincode/exectransaction_test.go

+21-20
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ func closeListenerAndSleep(l net.Listener) {
387387
}
388388
}
389389

390-
func executeDeployTransaction(t *testing.T, url string) {
390+
func executeDeployTransaction(t *testing.T, name string, url string) {
391391
lis, err := initPeer()
392392
if err != nil {
393393
t.Fail()
@@ -400,7 +400,7 @@ func executeDeployTransaction(t *testing.T, url string) {
400400

401401
f := "init"
402402
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
403-
spec := &pb.ChaincodeSpec{Type: 1, ChaincodeID: &pb.ChaincodeID{Path: url}, CtorMsg: &pb.ChaincodeInput{Args: args}}
403+
spec := &pb.ChaincodeSpec{Type: 1, ChaincodeID: &pb.ChaincodeID{Name: name, Path: url}, CtorMsg: &pb.ChaincodeInput{Args: args}}
404404
_, err = deploy(ctxt, spec)
405405
chaincodeID := spec.ChaincodeID.Name
406406
if err != nil {
@@ -415,23 +415,23 @@ func executeDeployTransaction(t *testing.T, url string) {
415415

416416
// Test deploy of a transaction
417417
func TestExecuteDeployTransaction(t *testing.T) {
418-
executeDeployTransaction(t, "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01")
418+
executeDeployTransaction(t, "example01", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01")
419419
}
420420

421421
// Test deploy of a transaction with a GOPATH with multiple elements
422422
func TestGopathExecuteDeployTransaction(t *testing.T) {
423423
// add a trailing slash to GOPATH
424424
// and a couple of elements - it doesn't matter what they are
425425
os.Setenv("GOPATH", os.Getenv("GOPATH")+string(os.PathSeparator)+string(os.PathListSeparator)+"/tmp/foo"+string(os.PathListSeparator)+"/tmp/bar")
426-
executeDeployTransaction(t, "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01")
426+
executeDeployTransaction(t, "example01", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01")
427427
}
428428

429429
// Test deploy of a transaction with a chaincode over HTTP.
430430
func TestHTTPExecuteDeployTransaction(t *testing.T) {
431431
// The chaincode used here cannot be from the fabric repo
432432
// itself or it won't be downloaded because it will be found
433433
// in GOPATH, which would defeat the test
434-
executeDeployTransaction(t, "http://gopkg.in/mastersingh24/fabric-test-resources.v1")
434+
executeDeployTransaction(t, "example01", "http://gopkg.in/mastersingh24/fabric-test-resources.v1")
435435
}
436436

437437
// Check the correctness of the final state after transaction execution.
@@ -537,7 +537,7 @@ func TestExecuteInvokeTransaction(t *testing.T) {
537537
var ctxt = context.Background()
538538

539539
url := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
540-
chaincodeID := &pb.ChaincodeID{Path: url}
540+
chaincodeID := &pb.ChaincodeID{Name: "example02", Path: url}
541541

542542
args := []string{"a", "b", "10"}
543543
err = invokeExample02Transaction(ctxt, chaincodeID, args, true)
@@ -601,7 +601,7 @@ func TestExecuteQuery(t *testing.T) {
601601

602602
url := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
603603

604-
cID := &pb.ChaincodeID{Path: url}
604+
cID := &pb.ChaincodeID{Name: "example02", Path: url}
605605
f := "init"
606606
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
607607

@@ -658,7 +658,7 @@ func TestExecuteInvokeInvalidTransaction(t *testing.T) {
658658
var ctxt = context.Background()
659659

660660
url := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
661-
chaincodeID := &pb.ChaincodeID{Path: url}
661+
chaincodeID := &pb.ChaincodeID{Name: "example02", Path: url}
662662

663663
//FAIL, FAIL!
664664
args := []string{"x", "-1"}
@@ -707,7 +707,7 @@ func chaincodeInvokeChaincode(t *testing.T, user string) (err error) {
707707
// Deploy first chaincode
708708
url1 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
709709

710-
cID1 := &pb.ChaincodeID{Path: url1}
710+
cID1 := &pb.ChaincodeID{Name: "example02", Path: url1}
711711
f := "init"
712712
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
713713

@@ -729,7 +729,7 @@ func chaincodeInvokeChaincode(t *testing.T, user string) (err error) {
729729
// Deploy second chaincode
730730
url2 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example04"
731731

732-
cID2 := &pb.ChaincodeID{Path: url2}
732+
cID2 := &pb.ChaincodeID{Name: "example04", Path: url2}
733733
f = "init"
734734
args = util.ToChaincodeArgs(f, "e", "0")
735735

@@ -747,8 +747,9 @@ func chaincodeInvokeChaincode(t *testing.T, user string) (err error) {
747747

748748
time.Sleep(time.Second)
749749

750-
// Invoke second chaincode, which will inturn invoke the first chaincode
751-
f = "invoke"
750+
// Invoke second chaincode passing the first chaincode's name as first param,
751+
// which will inturn invoke the first chaincode
752+
f = spec1.ChaincodeID.Name
752753
args = util.ToChaincodeArgs(f, "e", "1")
753754

754755
spec2 = &pb.ChaincodeSpec{Type: 1, ChaincodeID: cID2, CtorMsg: &pb.ChaincodeInput{Args: args}, SecureContext: user}
@@ -796,7 +797,7 @@ func TestChaincodeInvokeChaincodeErrorCase(t *testing.T) {
796797
// Deploy first chaincode
797798
url1 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
798799

799-
cID1 := &pb.ChaincodeID{Path: url1}
800+
cID1 := &pb.ChaincodeID{Name: "example02", Path: url1}
800801
f := "init"
801802
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
802803

@@ -816,7 +817,7 @@ func TestChaincodeInvokeChaincodeErrorCase(t *testing.T) {
816817
// Deploy second chaincode
817818
url2 := "github.com/hyperledger/fabric/examples/chaincode/go/passthru"
818819

819-
cID2 := &pb.ChaincodeID{Path: url2}
820+
cID2 := &pb.ChaincodeID{Name: "pthru", Path: url2}
820821
f = "init"
821822
args = util.ToChaincodeArgs(f)
822823

@@ -868,7 +869,7 @@ func chaincodeQueryChaincode(user string) error {
868869
// Deploy first chaincode
869870
url1 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
870871

871-
cID1 := &pb.ChaincodeID{Path: url1}
872+
cID1 := &pb.ChaincodeID{Name: "example02", Path: url1}
872873
f := "init"
873874
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
874875

@@ -886,7 +887,7 @@ func chaincodeQueryChaincode(user string) error {
886887
// Deploy second chaincode
887888
url2 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example05"
888889

889-
cID2 := &pb.ChaincodeID{Path: url2}
890+
cID2 := &pb.ChaincodeID{Name: "example05", Path: url2}
890891
f = "init"
891892
args = util.ToChaincodeArgs(f, "sum", "0")
892893

@@ -994,7 +995,7 @@ func TestChaincodeQueryChaincodeErrorCase(t *testing.T) {
994995
// Deploy first chaincode
995996
url1 := "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02"
996997

997-
cID1 := &pb.ChaincodeID{Path: url1}
998+
cID1 := &pb.ChaincodeID{Name: "example02", Path: url1}
998999
f := "init"
9991000
args := util.ToChaincodeArgs(f, "a", "100", "b", "200")
10001001

@@ -1014,7 +1015,7 @@ func TestChaincodeQueryChaincodeErrorCase(t *testing.T) {
10141015
// Deploy second chaincode
10151016
url2 := "github.com/hyperledger/fabric/examples/chaincode/go/passthru"
10161017

1017-
cID2 := &pb.ChaincodeID{Path: url2}
1018+
cID2 := &pb.ChaincodeID{Name: "pthru", Path: url2}
10181019
f = "init"
10191020
args = util.ToChaincodeArgs(f)
10201021

@@ -1128,7 +1129,7 @@ func TestRangeQuery(t *testing.T) {
11281129
var ctxt = context.Background()
11291130

11301131
url := "github.com/hyperledger/fabric/examples/chaincode/go/map"
1131-
cID := &pb.ChaincodeID{Path: url}
1132+
cID := &pb.ChaincodeID{Name: "tmap", Path: url}
11321133

11331134
f := "init"
11341135
args := util.ToChaincodeArgs(f)
@@ -1173,7 +1174,7 @@ func TestGetEvent(t *testing.T) {
11731174

11741175
url := "github.com/hyperledger/fabric/examples/chaincode/go/eventsender"
11751176

1176-
cID := &pb.ChaincodeID{Path: url}
1177+
cID := &pb.ChaincodeID{Name: "esender", Path: url}
11771178
f := "init"
11781179
spec := &pb.ChaincodeSpec{Type: 1, ChaincodeID: cID, CtorMsg: &pb.ChaincodeInput{Args: util.ToChaincodeArgs(f)}}
11791180

core/chaincode/lccc.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package chaincode
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"github.com/golang/protobuf/proto"
2324
"github.com/hyperledger/fabric/core/chaincode/shim"
@@ -53,6 +54,9 @@ const (
5354

5455
//GETDEPSPEC get ChaincodeDeploymentSpec
5556
GETDEPSPEC = "getdepspec"
57+
58+
//characters used in chaincodenamespace
59+
specialChars = "/:[]${}"
5660
)
5761

5862
//---------- the LCCC -----------------
@@ -227,7 +231,7 @@ func (lccc *LifeCycleSysCC) acl(stub shim.ChaincodeStubInterface, chainname Chai
227231

228232
//check validity of chain name
229233
func (lccc *LifeCycleSysCC) isValidChainName(chainname string) bool {
230-
//TODO we probably need more checks and have
234+
//TODO we probably need more checks
231235
if chainname == "" {
232236
return false
233237
}
@@ -236,10 +240,16 @@ func (lccc *LifeCycleSysCC) isValidChainName(chainname string) bool {
236240

237241
//check validity of chaincode name
238242
func (lccc *LifeCycleSysCC) isValidChaincodeName(chaincodename string) bool {
239-
//TODO we probably need more checks and have
243+
//TODO we probably need more checks
240244
if chaincodename == "" {
241245
return false
242246
}
247+
248+
//do not allow special characters in chaincode name
249+
if strings.ContainsAny(chaincodename, specialChars) {
250+
return false
251+
}
252+
243253
return true
244254
}
245255

core/chaincode/lccc_test.go

+32-8
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ func register(stub *shim.MockStub, ccname string) error {
3434
return nil
3535
}
3636

37-
func constructDeploymentSpec(path string, initArgs [][]byte) (*pb.ChaincodeDeploymentSpec, error) {
38-
spec := &pb.ChaincodeSpec{Type: 1, ChaincodeID: &pb.ChaincodeID{Path: path}, CtorMsg: &pb.ChaincodeInput{Args: initArgs}}
37+
func constructDeploymentSpec(name string, path string, initArgs [][]byte) (*pb.ChaincodeDeploymentSpec, error) {
38+
spec := &pb.ChaincodeSpec{Type: 1, ChaincodeID: &pb.ChaincodeID{Name: name, Path: path}, CtorMsg: &pb.ChaincodeInput{Args: initArgs}}
3939
codePackageBytes, err := container.GetChaincodePackageBytes(spec)
4040
if err != nil {
4141
return nil, err
@@ -67,7 +67,7 @@ func TestDeploy(t *testing.T) {
6767
scc := new(LifeCycleSysCC)
6868
stub := shim.NewMockStub("lccc", scc)
6969

70-
cds, err := constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
70+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
7171
var b []byte
7272
if b, err = proto.Marshal(cds); err != nil || b == nil {
7373
t.FailNow()
@@ -94,14 +94,38 @@ func TestInvalidCodeDeploy(t *testing.T) {
9494
}
9595
}
9696

97+
//TestInvalidChaincodeName tests the deploy function with invalid chaincode name
98+
func TestInvalidChaincodeName(t *testing.T) {
99+
initialize()
100+
101+
scc := new(LifeCycleSysCC)
102+
stub := shim.NewMockStub("lccc", scc)
103+
104+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
105+
106+
//change name to empty
107+
cds.ChaincodeSpec.ChaincodeID.Name = ""
108+
109+
var b []byte
110+
if b, err = proto.Marshal(cds); err != nil || b == nil {
111+
t.FailNow()
112+
}
113+
114+
args := [][]byte{[]byte(DEPLOY), []byte("test"), b}
115+
_, err = stub.MockInvoke("1", args)
116+
if _, ok := err.(InvalidChaincodeNameErr); !ok {
117+
t.FailNow()
118+
}
119+
}
120+
97121
//TestRedeploy tests the redeploying will fail function(and fail with "exists" error)
98122
func TestRedeploy(t *testing.T) {
99123
initialize()
100124

101125
scc := new(LifeCycleSysCC)
102126
stub := shim.NewMockStub("lccc", scc)
103127

104-
cds, err := constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
128+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
105129
var b []byte
106130
if b, err = proto.Marshal(cds); err != nil || b == nil {
107131
t.FailNow()
@@ -127,7 +151,7 @@ func TestCheckCC(t *testing.T) {
127151
scc := new(LifeCycleSysCC)
128152
stub := shim.NewMockStub("lccc", scc)
129153

130-
cds, err := constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
154+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
131155
var b []byte
132156
if b, err = proto.Marshal(cds); err != nil || b == nil {
133157
t.FailNow()
@@ -152,7 +176,7 @@ func TestMultipleDeploy(t *testing.T) {
152176
stub := shim.NewMockStub("lccc", scc)
153177

154178
//deploy 02
155-
cds, err := constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
179+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
156180
var b []byte
157181
if b, err = proto.Marshal(cds); err != nil || b == nil {
158182
t.FailNow()
@@ -169,7 +193,7 @@ func TestMultipleDeploy(t *testing.T) {
169193
}
170194

171195
//deploy 01
172-
cds, err = constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
196+
cds, err = constructDeploymentSpec("example01", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example01", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
173197
if b, err = proto.Marshal(cds); err != nil || b == nil {
174198
t.FailNow()
175199
}
@@ -193,7 +217,7 @@ func TestRetryFailedDeploy(t *testing.T) {
193217
stub := shim.NewMockStub("lccc", scc)
194218

195219
//deploy 02
196-
cds, err := constructDeploymentSpec("github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
220+
cds, err := constructDeploymentSpec("example02", "github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02", [][]byte{[]byte("init"), []byte("a"), []byte("100"), []byte("b"), []byte("200")})
197221
var b []byte
198222
if b, err = proto.Marshal(cds); err != nil || b == nil {
199223
t.FailNow()

core/chaincode/platforms/car/package.go

-5
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,6 @@ func (carPlatform *Platform) WritePackage(spec *pb.ChaincodeSpec, tw *tar.Writer
6969
return err
7070
}
7171

72-
spec.ChaincodeID.Name, err = generateHashcode(spec, path)
73-
if err != nil {
74-
return fmt.Errorf("Error generating hashcode: %s", err)
75-
}
76-
7772
var buf []string
7873

7974
//let the executable's name be chaincode ID's name

core/chaincode/platforms/car/test/car_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestCar_BuildImage(t *testing.T) {
4848
}
4949

5050
chaincodePath := cwd + "/org.hyperledger.chaincode.example02-0.1-SNAPSHOT.car"
51-
spec := &pb.ChaincodeSpec{Type: pb.ChaincodeSpec_CAR, ChaincodeID: &pb.ChaincodeID{Path: chaincodePath}, CtorMsg: &pb.ChaincodeInput{Args: util.ToChaincodeArgs("f")}}
51+
spec := &pb.ChaincodeSpec{Type: pb.ChaincodeSpec_CAR, ChaincodeID: &pb.ChaincodeID{Name: "cartest", Path: chaincodePath}, CtorMsg: &pb.ChaincodeInput{Args: util.ToChaincodeArgs("f")}}
5252
if _, err := vm.BuildChaincodeContainer(spec); err != nil {
5353
t.Fail()
5454
t.Log(err)

core/chaincode/platforms/golang/hash.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -210,25 +210,25 @@ func getCodeFromFS(path string) (codegopath string, err error) {
210210
return
211211
}
212212

213-
//generateHashcode gets hashcode of the code under path. If path is a HTTP(s) url
214-
//it downloads the code first to compute the hash.
213+
//collectChaincodeFiles collects chaincode files and generates hashcode for the
214+
//package. If path is a HTTP(s) url it downloads the code first.
215215
//NOTE: for dev mode, user builds and runs chaincode manually. The name provided
216216
//by the user is equivalent to the path. This method will treat the name
217217
//as codebytes and compute the hash from it. ie, user cannot run the chaincode
218218
//with the same (name, ctor, args)
219-
func generateHashcode(spec *pb.ChaincodeSpec, tw *tar.Writer) (string, error) {
219+
func collectChaincodeFiles(spec *pb.ChaincodeSpec, tw *tar.Writer) (string, error) {
220220
if spec == nil {
221-
return "", fmt.Errorf("Cannot generate hashcode from nil spec")
221+
return "", fmt.Errorf("Cannot collect files from nil spec")
222222
}
223223

224224
chaincodeID := spec.ChaincodeID
225225
if chaincodeID == nil || chaincodeID.Path == "" {
226-
return "", fmt.Errorf("Cannot generate hashcode from empty chaincode path")
226+
return "", fmt.Errorf("Cannot collect files from empty chaincode path")
227227
}
228228

229229
ctor := spec.CtorMsg
230230
if ctor == nil || len(ctor.Args) == 0 {
231-
return "", fmt.Errorf("Cannot generate hashcode from empty ctor")
231+
return "", fmt.Errorf("Cannot collect files from empty ctor")
232232
}
233233

234234
//code root will point to the directory where the code exists

core/chaincode/platforms/golang/platform.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ func (goPlatform *Platform) ValidateSpec(spec *pb.ChaincodeSpec) error {
7272
func (goPlatform *Platform) WritePackage(spec *pb.ChaincodeSpec, tw *tar.Writer) error {
7373

7474
var err error
75-
spec.ChaincodeID.Name, err = generateHashcode(spec, tw)
75+
76+
//ignore the generated hash. Just use the tw
77+
//The hash could be used in a future enhancement
78+
//to check, warn of duplicate installs etc.
79+
_, err = collectChaincodeFiles(spec, tw)
7680
if err != nil {
7781
return err
7882
}

0 commit comments

Comments
 (0)