Skip to content

Commit

Permalink
Merge pull request #636 from sandrask/issue-635
Browse files Browse the repository at this point in the history
feat: Allow update without published create operation
  • Loading branch information
sandrask authored Jan 24, 2022
2 parents bceddd3 + 4ac7a38 commit 717ab30
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 28 deletions.
35 changes: 33 additions & 2 deletions pkg/dochandler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,11 @@ func GetTransformationInfoForUnpublished(namespace, domain, label, suffix string

// Also, if optional domain is specified, we should set equivalent id with domain hint
if label != "" && domain != "" {
equivalentID := fmt.Sprintf("%s:%s:%s:%s", namespace, domain, label, suffix)
equivalentID := id
if !strings.Contains(label, domain) {
equivalentID = fmt.Sprintf("%s:%s:%s:%s", namespace, domain, label, suffix)
}

equivalentIDs = append(equivalentIDs, equivalentID)
}

Expand Down Expand Up @@ -427,11 +431,38 @@ func (r *DocumentHandler) resolveRequestWithID(shortFormDid, uniquePortion strin
return nil, err
}

ti := GetTransformationInfoForPublished(r.namespace, shortFormDid, uniquePortion, internalResult)
var ti protocol.TransformationInfo

if len(internalResult.PublishedOperations) == 0 {
hint, err := GetHint(shortFormDid, r.namespace, uniquePortion)
if err != nil {
return nil, err
}

ti = GetTransformationInfoForUnpublished(r.namespace, r.domain, hint, uniquePortion, "")
} else {
ti = GetTransformationInfoForPublished(r.namespace, shortFormDid, uniquePortion, internalResult)
}

return pv.DocumentTransformer().TransformDocument(internalResult, ti)
}

// GetHint returns hint from id.
func GetHint(id, namespace, suffix string) (string, error) {
posSuffix := strings.LastIndex(id, suffix)
if posSuffix == -1 {
return "", fmt.Errorf("invalid ID [%s]", id)
}

if len(namespace)+1 > posSuffix-1 {
return "", nil
}

hint := id[len(namespace)+1 : posSuffix-1]

return hint, nil
}

// GetTransformationInfoForPublished will create transformation info object for published document.
func GetTransformationInfoForPublished(namespace, id, suffix string,
internalResult *protocol.ResolutionModel) protocol.TransformationInfo {
Expand Down
133 changes: 132 additions & 1 deletion pkg/dochandler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func TestDocumentHandler_ProcessOperation_Update(t *testing.T) {
t.Run("success - unpublished operation store option", func(t *testing.T) {
store := mocks.NewMockOperationStore(nil)

opt := WithUnpublishedOperationStore(&noopUnpublishedOpsStore{}, []operation.Type{operation.TypeUpdate})
opt := WithUnpublishedOperationStore(&mockUnpublishedOpsStore{}, []operation.Type{operation.TypeUpdate})

dochandler, cleanup := getDocumentHandler(store, opt)
require.NotNil(t, dochandler)
Expand All @@ -223,6 +223,100 @@ func TestDocumentHandler_ProcessOperation_Update(t *testing.T) {
require.Nil(t, doc)
})

t.Run("success - unpublished operation store option(create and update)", func(t *testing.T) {
store := mocks.NewMockOperationStore(nil)

createOp := getCreateOperation()

updateOp, err := generateUpdateOperation(createOp.UniqueSuffix)
require.NoError(t, err)

unpublishedOperationStore := &mockUnpublishedOpsStore{
Ops: []*operation.AnchoredOperation{
{
Type: "create",
OperationRequest: createOp.OperationRequest,
UniqueSuffix: createOp.UniqueSuffix,
},
},
}

protocol := newMockProtocolClient()

processor := processor.New("test", store, protocol, processor.WithUnpublishedOperationStore(unpublishedOperationStore))

ctx := &BatchContext{
ProtocolClient: protocol,
CasClient: mocks.NewMockCasClient(nil),
AnchorWriter: mocks.NewMockAnchorWriter(nil),
OpQueue: &opqueue.MemQueue{},
}
writer, err := batch.New("test", ctx)
if err != nil {
panic(err)
}

// start go routine for cutting batches
writer.Start()

dochandler, cleanup := New(namespace, []string{alias}, protocol, writer, processor, &mocks.MetricsProvider{},
WithUnpublishedOperationStore(unpublishedOperationStore, []operation.Type{operation.TypeCreate, operation.TypeUpdate})), func() { writer.Stop() }
require.NotNil(t, dochandler)
defer cleanup()

doc, err := dochandler.ProcessOperation(updateOp, 0)
require.NoError(t, err)
require.Nil(t, doc)

doc, err = dochandler.ResolveDocument(createOp.ID)
require.NoError(t, err)
fmt.Printf("%+v", doc)

idWithHint := namespace + ":domain.com" + createOp.UniqueSuffix

doc, err = dochandler.ResolveDocument(idWithHint)
require.NoError(t, err)
})

t.Run("error - update without unpublished/published create", func(t *testing.T) {
store := mocks.NewMockOperationStore(nil)

createOp := getCreateOperation()

updateOp, err := generateUpdateOperation(createOp.UniqueSuffix)
require.NoError(t, err)

unpublishedOperationStore := &mockUnpublishedOpsStore{}

protocol := newMockProtocolClient()

processor := processor.New("test", store, protocol, processor.WithUnpublishedOperationStore(unpublishedOperationStore))

ctx := &BatchContext{
ProtocolClient: protocol,
CasClient: mocks.NewMockCasClient(nil),
AnchorWriter: mocks.NewMockAnchorWriter(nil),
OpQueue: &opqueue.MemQueue{},
}
writer, err := batch.New("test", ctx)
if err != nil {
panic(err)
}

// start go routine for cutting batches
writer.Start()

dochandler, cleanup := New(namespace, []string{alias}, protocol, writer, processor, &mocks.MetricsProvider{},
WithUnpublishedOperationStore(unpublishedOperationStore, []operation.Type{operation.TypeCreate, operation.TypeUpdate})), func() { writer.Stop() }
require.NotNil(t, dochandler)
defer cleanup()

doc, err := dochandler.ProcessOperation(updateOp, 0)
require.Error(t, err)
require.Nil(t, doc)
require.Contains(t, err.Error(), "bad request: create operation not found")
})

t.Run("error - batch writer error (unpublished operation store option)", func(t *testing.T) {
store := mocks.NewMockOperationStore(nil)

Expand Down Expand Up @@ -676,6 +770,33 @@ func TestProcessOperation_ParseOperationError(t *testing.T) {
require.Contains(t, err.Error(), "bad request: missing signed data")
}

func TestGetHint(t *testing.T) {
const namespace = "did:sidetree"
const testID = "did:sidetree:unique"

t.Run("success", func(t *testing.T) {
hint, err := GetHint("did:sidetree:hint:unique", namespace, "unique")
require.NoError(t, err)
require.Equal(t, "hint", hint)
})

t.Run("success - no hint", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
hint, err := GetHint(testID, namespace, "unique")
require.NoError(t, err)
require.Empty(t, hint)
})
})

t.Run("error - wrong suffix", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
hint, err := GetHint(testID, namespace, "other")
require.Error(t, err)
require.Empty(t, hint)
})
})
}

// BatchContext implements batch writer context.
type BatchContext struct {
ProtocolClient *mocks.MockProtocolClient
Expand Down Expand Up @@ -1024,8 +1145,10 @@ func newMockProtocolClient() *mocks.MockProtocolClient {
}

type mockUnpublishedOpsStore struct {
Ops []*operation.AnchoredOperation
PutErr error
DeleteErr error
GetErr error
}

func (m *mockUnpublishedOpsStore) Put(_ *operation.AnchoredOperation) error {
Expand All @@ -1036,6 +1159,14 @@ func (m *mockUnpublishedOpsStore) Delete(_ *operation.AnchoredOperation) error {
return m.DeleteErr
}

func (m *mockUnpublishedOpsStore) Get(uniqueSuffix string) ([]*operation.AnchoredOperation, error) {
if m.GetErr != nil {
return nil, m.GetErr
}

return m.Ops, nil
}

type mockOperationDecorator struct {
Err error
}
Expand Down
46 changes: 30 additions & 16 deletions pkg/processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"sort"
"strings"

"github.com/trustbloc/edge-core/pkg/log"

Expand Down Expand Up @@ -68,7 +69,7 @@ func WithUnpublishedOperationStore(store unpublishedOperationStore) Option {
// uniqueSuffix - unique portion of ID to resolve. for example "abc123" in "did:sidetree:abc123".
func (s *OperationProcessor) Resolve(uniqueSuffix string, additionalOps ...*operation.AnchoredOperation) (*protocol.ResolutionModel, error) {
publishedOps, err := s.store.Get(uniqueSuffix)
if err != nil {
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, err
}

Expand All @@ -94,7 +95,7 @@ func (s *OperationProcessor) Resolve(uniqueSuffix string, additionalOps ...*oper
// split operations into 'create', 'update' and 'full' operations
createOps, updateOps, fullOps := splitOperations(ops)
if len(createOps) == 0 {
return nil, errors.New("missing create operation")
return nil, fmt.Errorf("create operation not found")
}

// apply 'create' operations first
Expand All @@ -115,7 +116,7 @@ func (s *OperationProcessor) Resolve(uniqueSuffix string, additionalOps ...*oper
}

// next apply update ops since last 'full' transaction
filteredUpdateOps := getOpsWithTxnGreaterThan(updateOps, rm.LastOperationTransactionTime, rm.LastOperationTransactionNumber)
filteredUpdateOps := getOpsWithTxnGreaterThanOrUnpublished(updateOps, rm.LastOperationTransactionTime, rm.LastOperationTransactionNumber)
if len(filteredUpdateOps) > 0 {
logger.Debugf("[%s] Applying %d update operations after last full operation for unique suffix [%s]", s.name, len(filteredUpdateOps), uniqueSuffix)
rm = s.applyOperations(filteredUpdateOps, rm, getUpdateCommitment)
Expand Down Expand Up @@ -192,23 +193,36 @@ func splitOperations(ops []*operation.AnchoredOperation) (createOps, updateOps,
return createOps, updateOps, fullOps
}

// pre-condition: operations have to be sorted.
func getOpsWithTxnGreaterThan(ops []*operation.AnchoredOperation, txnTime, txnNumber uint64) []*operation.AnchoredOperation {
for index, op := range ops {
if op.TransactionTime < txnTime {
continue
}
func getOpsWithTxnGreaterThanOrUnpublished(ops []*operation.AnchoredOperation, txnTime, txnNumber uint64) []*operation.AnchoredOperation {
var selection []*operation.AnchoredOperation

if op.TransactionTime > txnTime {
return ops[index:]
for _, op := range ops {
if isOpWithTxnGreaterThanOrUnpublished(op, txnTime, txnNumber) {
selection = append(selection, op)
}
}

if op.TransactionNumber > txnNumber {
return ops[index:]
}
return selection
}

func isOpWithTxnGreaterThanOrUnpublished(op *operation.AnchoredOperation, txnTime, txnNumber uint64) bool {
if op.CanonicalReference == "" {
return true
}

return nil
if op.TransactionTime < txnTime {
return false
}

if op.TransactionTime > txnTime {
return true
}

if op.TransactionNumber > txnNumber {
return true
}

return false
}

func (s *OperationProcessor) applyOperations(ops []*operation.AnchoredOperation, rm *protocol.ResolutionModel, commitmentFnc fnc) *protocol.ResolutionModel {
Expand Down Expand Up @@ -328,7 +342,7 @@ func (s *OperationProcessor) applyFirstValidOperation(ops []*operation.AnchoredO
continue
}

logger.Debugf("[%s] After applying op %+v, recover commitment[%s], update commitment[%s], New doc: %s", s.name, op, state.RecoveryCommitment, state.UpdateCommitment, state.Doc)
logger.Debugf("[%s] After applying op %+v, recover commitment[%s], update commitment[%s], deactivated[%d] New doc: %s", s.name, op, state.RecoveryCommitment, state.UpdateCommitment, state.Deactivated, state.Doc)

return state
}
Expand Down
20 changes: 11 additions & 9 deletions pkg/processor/processor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestResolve(t *testing.T) {
doc, err := op.Resolve(dummyUniqueSuffix)
require.Nil(t, doc)
require.Error(t, err)
require.Equal(t, "uniqueSuffix not found in the store", err.Error())
require.Equal(t, "create operation not found", err.Error())
})

t.Run("store error", func(t *testing.T) {
Expand Down Expand Up @@ -390,7 +390,7 @@ func TestProcessOperation(t *testing.T) {
doc, err := p.Resolve(uniqueSuffix)
require.Error(t, err)
require.Nil(t, doc)
require.Equal(t, "missing create operation", err.Error())
require.Equal(t, "create operation not found", err.Error())
})

t.Run("create is second operation error", func(t *testing.T) {
Expand Down Expand Up @@ -862,24 +862,26 @@ func TestGetNextOperationCommitment(t *testing.T) {

func TestOpsWithTxnGreaterThan(t *testing.T) {
op1 := &operation.AnchoredOperation{
TransactionTime: 1,
TransactionNumber: 1,
TransactionTime: 1,
TransactionNumber: 1,
CanonicalReference: "ref",
}

op2 := &operation.AnchoredOperation{
TransactionTime: 1,
TransactionNumber: 2,
TransactionTime: 1,
TransactionNumber: 2,
CanonicalReference: "ref",
}

ops := []*operation.AnchoredOperation{op1, op2}

txns := getOpsWithTxnGreaterThan(ops, 0, 0)
txns := getOpsWithTxnGreaterThanOrUnpublished(ops, 0, 0)
require.Equal(t, 2, len(txns))

txns = getOpsWithTxnGreaterThan(ops, 2, 1)
txns = getOpsWithTxnGreaterThanOrUnpublished(ops, 2, 1)
require.Equal(t, 0, len(txns))

txns = getOpsWithTxnGreaterThan(ops, 1, 1)
txns = getOpsWithTxnGreaterThanOrUnpublished(ops, 1, 1)
require.Equal(t, 1, len(txns))
}

Expand Down

0 comments on commit 717ab30

Please sign in to comment.