Skip to content

Commit cff913f

Browse files
committed
Maintain lexographic sort order for composite keys
https://jira.hyperledger.org/browse/FAB-2008 This CR - Changes the way composite key and prefix scan range are encoded so as to maintain the lexographical sort order of the results of a range query - Cleans up escaping used in couchdb for start/end Key for range scan - Adds couchdb test for a prefix scan Change-Id: Icbc95ddf3916b5286a22eb83c173efd8dc68e75d Signed-off-by: manish <[email protected]>
1 parent e855f8e commit cff913f

File tree

7 files changed

+177
-65
lines changed

7 files changed

+177
-65
lines changed

core/chaincode/shim/chaincode.go

+45-25
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@ limitations under the License.
1919
package shim
2020

2121
import (
22-
"bytes"
2322
"errors"
2423
"flag"
2524
"fmt"
2625
"io"
2726
"os"
28-
"regexp"
29-
"strconv"
3027
"strings"
28+
"unicode/utf8"
3129

3230
"github.com/golang/protobuf/proto"
3331
"github.com/golang/protobuf/ptypes/timestamp"
@@ -43,6 +41,11 @@ import (
4341
// Logger for the shim package.
4442
var chaincodeLogger = logging.MustGetLogger("shim")
4543

44+
const (
45+
minUnicodeRuneValue = 0 //U+0000
46+
maxUnicodeRuneValue = utf8.MaxRune //U+10FFFF - maximum (and unallocated) code point
47+
)
48+
4649
// ChaincodeStub is an object passed to chaincode for shim side handling of
4750
// APIs.
4851
type ChaincodeStub struct {
@@ -359,35 +362,52 @@ func (stub *ChaincodeStub) GetQueryResult(query string) (StateQueryIteratorInter
359362

360363
//CreateCompositeKey combines the given attributes to form a composite key.
361364
func (stub *ChaincodeStub) CreateCompositeKey(objectType string, attributes []string) (string, error) {
362-
return createCompositeKey(stub, objectType, attributes)
365+
return createCompositeKey(objectType, attributes)
366+
}
367+
368+
//SplitCompositeKey splits the key into attributes on which the composite key was formed.
369+
func (stub *ChaincodeStub) SplitCompositeKey(compositeKey string) (string, []string, error) {
370+
return splitCompositeKey(compositeKey)
363371
}
364372

365-
func createCompositeKey(stub ChaincodeStubInterface, objectType string, attributes []string) (string, error) {
366-
var compositeKey bytes.Buffer
367-
replacer := strings.NewReplacer("\x1E", "\x1E\x1E", "\x1F", "\x1E\x1F")
368-
compositeKey.WriteString(replacer.Replace(objectType))
369-
for _, attribute := range attributes {
370-
compositeKey.WriteString("\x1F" + strconv.Itoa(len(attribute)) + "\x1F")
371-
compositeKey.WriteString(replacer.Replace(attribute))
373+
func createCompositeKey(objectType string, attributes []string) (string, error) {
374+
if err := validateCompositeKeyAttribute(objectType); err != nil {
375+
return "", err
376+
}
377+
ck := objectType + string(minUnicodeRuneValue)
378+
for _, att := range attributes {
379+
if err := validateCompositeKeyAttribute(att); err != nil {
380+
return "", err
381+
}
382+
ck += att + string(minUnicodeRuneValue)
372383
}
373-
return compositeKey.String(), nil
384+
return ck, nil
374385
}
375386

376-
//SplitCompositeKey splits the key into attributes on which the composite key was formed.
377-
func (stub *ChaincodeStub) SplitCompositeKey(compositeKey string) (string, []string, error) {
378-
return splitCompositeKey(stub, compositeKey)
387+
func splitCompositeKey(compositeKey string) (string, []string, error) {
388+
componentIndex := 0
389+
components := []string{}
390+
for i := 0; i < len(compositeKey); i++ {
391+
if compositeKey[i] == minUnicodeRuneValue {
392+
components = append(components, compositeKey[componentIndex:i])
393+
componentIndex = i + 1
394+
}
395+
}
396+
return components[0], components[1:], nil
379397
}
380398

381-
func splitCompositeKey(stub ChaincodeStubInterface, compositeKey string) (string, []string, error) {
382-
re := regexp.MustCompile("\x1F[0-9]+\x1F")
383-
splittedKey := re.Split(compositeKey, -1)
384-
attributes := make([]string, 0)
385-
replacer := strings.NewReplacer("\x1E\x1F", "\x1F", "\x1E\x1E", "\x1E")
386-
objectType := replacer.Replace(splittedKey[0])
387-
for _, attr := range splittedKey[1:] {
388-
attributes = append(attributes, replacer.Replace(attr))
399+
func validateCompositeKeyAttribute(str string) error {
400+
for index, runeValue := range str {
401+
if !utf8.ValidRune(runeValue) {
402+
return fmt.Errorf("Not a valid utf8 string. Contains rune [%d] starting at byte position [%d]",
403+
runeValue, index)
404+
}
405+
if runeValue == minUnicodeRuneValue || runeValue == maxUnicodeRuneValue {
406+
return fmt.Errorf(`Input contain unicode %#U starting at position [%d]. %#U and %#U are not allowed in the input attribute of a composite key`,
407+
runeValue, index, minUnicodeRuneValue, maxUnicodeRuneValue)
408+
}
389409
}
390-
return objectType, attributes, nil
410+
return nil
391411
}
392412

393413
//PartialCompositeKeyQuery function can be invoked by a chaincode to query the
@@ -402,7 +422,7 @@ func (stub *ChaincodeStub) PartialCompositeKeyQuery(objectType string, attribute
402422

403423
func partialCompositeKeyQuery(stub ChaincodeStubInterface, objectType string, attributes []string) (StateQueryIteratorInterface, error) {
404424
partialCompositeKey, _ := stub.CreateCompositeKey(objectType, attributes)
405-
keysIter, err := stub.RangeQueryState(partialCompositeKey, partialCompositeKey+"\xFF")
425+
keysIter, err := stub.RangeQueryState(partialCompositeKey, partialCompositeKey+string(maxUnicodeRuneValue))
406426
if err != nil {
407427
return nil, fmt.Errorf("Error fetching rows: %s", err)
408428
}

core/chaincode/shim/interfaces.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,26 @@ type ChaincodeStubInterface interface {
7777
// iterator which can be used to iterate over all composite keys whose prefix
7878
// matches the given partial composite key. This function should be used only for
7979
// a partial composite key. For a full composite key, an iter with empty response
80-
// would be returned.
80+
// would be returned. The objectType and attributes are expected to have only
81+
// valid utf8 strings and should not contain U+0000 (nil byte) and U+10FFFF (biggest and unallocated code point)
8182
PartialCompositeKeyQuery(objectType string, keys []string) (StateQueryIteratorInterface, error)
8283

8384
// Given a list of attributes, CreateCompositeKey function combines these attributes
84-
// to form a composite key.
85+
// to form a composite key. The objectType and attributes are expected to have only
86+
// valid utf8 strings and should not contain U+0000 (nil byte) and U+10FFFF (biggest and unallocated code point)
8587
CreateCompositeKey(objectType string, attributes []string) (string, error)
8688

89+
// Given a composite key, SplitCompositeKey function splits the key into attributes
90+
// on which the composite key was formed.
91+
SplitCompositeKey(compositeKey string) (string, []string, error)
92+
8793
// GetQueryResult function can be invoked by a chaincode to perform a
8894
// rich query against state database. Only supported by state database implementations
8995
// that support rich query. The query string is in the syntax of the underlying
9096
// state database. An iterator is returned which can be used to iterate (next) over
9197
// the query result set
9298
GetQueryResult(query string) (StateQueryIteratorInterface, error)
9399

94-
// Given a composite key, SplitCompositeKey function splits the key into attributes
95-
// on which the composite key was formed.
96-
SplitCompositeKey(compositeKey string) (string, []string, error)
97-
98100
// GetCallerCertificate returns caller certificate
99101
GetCallerCertificate() ([]byte, error)
100102

core/chaincode/shim/mockstub.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,13 @@ func (stub *MockStub) PartialCompositeKeyQuery(objectType string, attributes []s
217217
// CreateCompositeKey combines the list of attributes
218218
//to form a composite key.
219219
func (stub *MockStub) CreateCompositeKey(objectType string, attributes []string) (string, error) {
220-
return createCompositeKey(stub, objectType, attributes)
220+
return createCompositeKey(objectType, attributes)
221221
}
222222

223223
// SplitCompositeKey splits the composite key into attributes
224224
// on which the composite key was formed.
225225
func (stub *MockStub) SplitCompositeKey(compositeKey string) (string, []string, error) {
226-
return splitCompositeKey(stub, compositeKey)
226+
return splitCompositeKey(compositeKey)
227227
}
228228

229229
// InvokeChaincode calls a peered chaincode.

core/chaincode/shim/mockstub_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ func TestPartialCompositeKeyQuery(t *testing.T) {
111111
stub.PutState(compositeKey3, marbleJSONBytes3)
112112

113113
stub.MockTransactionEnd("init")
114-
expectKeys := []string{compositeKey1, compositeKey2}
115-
expectKeysAttributes := [][]string{{"set-1", "red"}, {"set-1", "blue"}}
116-
expectValues := [][]byte{marbleJSONBytes1, marbleJSONBytes2}
114+
// should return in sorted order of attributes
115+
expectKeys := []string{compositeKey2, compositeKey1}
116+
expectKeysAttributes := [][]string{{"set-1", "blue"}, {"set-1", "red"}}
117+
expectValues := [][]byte{marbleJSONBytes2, marbleJSONBytes1}
117118

118119
rqi, _ := stub.PartialCompositeKeyQuery("marble", []string{"set-1"})
119120
fmt.Println("Running loop")

core/ledger/util/couchdb/couchdb.go

+40-22
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"regexp"
3434
"strconv"
3535
"strings"
36+
"unicode/utf8"
3637

3738
"github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/version"
3839
logging "github.com/op/go-logging"
@@ -335,16 +336,18 @@ func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error)
335336

336337
//SaveDoc method provides a function to save a document, id and byte array
337338
func (dbclient *CouchDatabase) SaveDoc(id string, rev string, bytesDoc []byte, attachments []Attachment) (string, error) {
338-
339339
logger.Debugf("Entering SaveDoc()")
340-
340+
if !utf8.ValidString(id) {
341+
return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
342+
}
341343
saveURL, err := url.Parse(dbclient.couchInstance.conf.URL)
342344
if err != nil {
343345
logger.Errorf("URL parse error: %s", err.Error())
344346
return "", err
345347
}
346-
saveURL.Path = dbclient.dbName + "/" + id
347-
348+
saveURL.Path = dbclient.dbName
349+
// id can contain a '/', so encode separately
350+
saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)}
348351
logger.Debugf(" id=%s, value=%s", id, string(bytesDoc))
349352

350353
if rev == "" {
@@ -501,14 +504,17 @@ func getRevisionHeader(resp *http.Response) (string, error) {
501504
func (dbclient *CouchDatabase) ReadDoc(id string) ([]byte, string, error) {
502505

503506
logger.Debugf("Entering ReadDoc() id=%s", id)
504-
507+
if !utf8.ValidString(id) {
508+
return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
509+
}
505510
readURL, err := url.Parse(dbclient.couchInstance.conf.URL)
506511
if err != nil {
507512
logger.Errorf("URL parse error: %s", err.Error())
508513
return nil, "", err
509514
}
510-
readURL.Path = dbclient.dbName + "/" + id
511-
515+
readURL.Path = dbclient.dbName
516+
// id can contain a '/', so encode separately
517+
readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)}
512518
query := readURL.Query()
513519
query.Add("attachments", "true")
514520

@@ -644,26 +650,19 @@ func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip
644650

645651
//Append the startKey if provided
646652
if startKey != "" {
647-
startKey = strconv.QuoteToGraphic(startKey)
648-
startKey = strings.Replace(startKey, "\\x00", "\\u0000", -1)
649-
startKey = strings.Replace(startKey, "\\x1e", "\\u001e", -1)
650-
startKey = strings.Replace(startKey, "\\x1f", "\\u001f", -1)
651-
startKey = strings.Replace(startKey, "\\xff", "\\u00ff", -1)
652-
//TODO add general unicode support instead of special cases
653-
653+
var err error
654+
if startKey, err = encodeForJSON(startKey); err != nil {
655+
return nil, err
656+
}
654657
queryParms.Add("startkey", startKey)
655658
}
656659

657660
//Append the endKey if provided
658661
if endKey != "" {
659-
endKey = strconv.QuoteToGraphic(endKey)
660-
endKey = strings.Replace(endKey, "\\x00", "\\u0000", -1)
661-
endKey = strings.Replace(endKey, "\\x01", "\\u0001", -1)
662-
endKey = strings.Replace(endKey, "\\x1e", "\\u001e", -1)
663-
endKey = strings.Replace(endKey, "\\x1f", "\\u001f", -1)
664-
endKey = strings.Replace(endKey, "\\xff", "\\u00ff", -1)
665-
//TODO add general unicode support instead of special cases
666-
662+
var err error
663+
if endKey, err = encodeForJSON(endKey); err != nil {
664+
return nil, err
665+
}
667666
queryParms.Add("endkey", endKey)
668667
}
669668

@@ -919,3 +918,22 @@ func IsJSON(s string) bool {
919918
var js map[string]interface{}
920919
return json.Unmarshal([]byte(s), &js) == nil
921920
}
921+
922+
// encodePathElement uses Golang for encoding and in addition, replaces a '/' by %2F.
923+
// Otherwise, in the regular encoding, a '/' is treated as a path separator in the url
924+
func encodePathElement(str string) string {
925+
u := &url.URL{}
926+
u.Path = str
927+
encodedStr := u.String()
928+
encodedStr = strings.Replace(encodedStr, "/", "%2F", -1)
929+
return encodedStr
930+
}
931+
932+
func encodeForJSON(str string) (string, error) {
933+
buf := &bytes.Buffer{}
934+
encoder := json.NewEncoder(buf)
935+
if err := encoder.Encode(str); err != nil {
936+
return "", err
937+
}
938+
return buf.String(), nil
939+
}

0 commit comments

Comments
 (0)