From a742b007969101afb9fd2a57b143492d48a698cc Mon Sep 17 00:00:00 2001 From: Miguel Hervas Date: Mon, 18 Mar 2019 10:51:24 +0100 Subject: [PATCH] Merge Develop (#855) * run tests in parallel (#824) * Key revoked validation (#820) * Revocation validation * Revocation validation * Review comments * add rules (#821) Fixs #800. * Feat(documents): validate transitions (#819) Closes #802 and Closes #803 * Implement CollaboratorCanUpdate on Invoice and PO (#830) Closes #805 and Closes #806 * Feat/integrate collab can update (#834) * added method to Model * validator * test * lint * New Signature structure (#791) * signature check * tmp * fix test * signature * fmt * fix tests * use repo * fixes * updating proto * added setup back * Feat/write acls receive doc (#840) Fixes #838 Also, removed creating identities in every test. this should improve our integration tests run time Note: testworld tests that check for transition failures are disabled until #807 is done * Update log for coredoc (#842) * Update log for coredoc * Update log for coredoc * Review comments * Merge * Update deps * Update deps * minor * Added Invalid Signature Tests (#784) * Initial commit for invalid signature test cases * Added invalid signature in the utility method * Added test case for revoked key signature * Fixed test case to use Alice key for signing document * Refactored prepareCoreDocument helper method * Refactored invalid signature test * Added valid signature success test case * Rebased with develop and fixed failing tests * Fixed issue with converting time to protobuf Timestamp * Fixed failing unit test for timestamp * Adding signing purpose key back once revoked * Refactored Add and Revoke signing key * Added method to get signing key pair * Fixed spacing issues with imports * Added test case to send document with revoked signing key * Resolved review comments * Refactored waitTillStatus to getTransactionStatusAndMessage * Added nil check for message in transaction * Document consensus validations (#846) * Document consensus validations * timestamp validations done * More validations * fix test * Fix tests * Review comments * transitionValidator integration: requestDocumentSignature (#841) Issue Link: closes #807. Co-authored-by: Vedhavyas Singareddi * Nft sign + execute (#845) * signature check * tmp * fix test * signature * fmt * fix tests * use repo * fixes * adding signature proof * updating proto * added setup back * execute integration * add setup * new contract * comments * fix tw * fix id test * fix p2p test * Fix timestamp conversions (#850) * Document consensus validations * timestamp validations done * More validations * fix test * Fix tests * Fix timestamp conversions * Review comments * Gocelery update * Fix logs * Fix logging * Secret as env (#852) * move secret (#853) * Adding cent prefix (#854) * Fix identity concurrent creation (#848) * WIP fix identity concurrent creation * tests * add factory check --- .travis.yml | 22 +- Gopkg.lock | 12 +- Gopkg.toml | 6 +- anchors/anchor_contract.go | 12 +- anchors/anchor_repository.go | 11 +- anchors/anchor_repository_integration_test.go | 12 +- anchors/service.go | 9 +- anchors/service_test.go | 19 +- build/configs/default_config.yaml | 12 +- build/scripts/docker/entrypoint.sh | 5 +- build/scripts/migrate.sh | 2 + build/scripts/test_wrapper.sh | 20 +- config/configstore/model.go | 11 +- contextutil/context.go | 2 +- crypto/keytools.go | 24 + documents/anchor_task.go | 3 +- documents/bootstrapper.go | 26 +- documents/coredocument.go | 275 +++++++---- documents/coredocument_test.go | 108 ++++- documents/documents_test/service_test.go | 214 +++++++-- documents/error.go | 6 + documents/invoice/model.go | 49 +- documents/invoice/model_test.go | 132 ++++- documents/invoice/service_test.go | 8 +- documents/model.go | 20 +- documents/processor.go | 14 +- documents/processor_test.go | 123 ++++- documents/purchaseorder/model.go | 49 +- documents/purchaseorder/model_test.go | 57 +++ documents/purchaseorder/service_test.go | 8 +- documents/read_acls.go | 60 ++- documents/read_acls_test.go | 6 +- documents/service.go | 62 ++- documents/validator.go | 137 +++++- documents/validator_test.go | 191 +++++++- documents/write_acls.go | 202 +++++++- documents/write_acls_test.go | 451 +++++++++++++++--- ethereum/geth_client.go | 20 +- ethereum/geth_client_integration_test.go | 1 + ethereum/transaction_status_task.go | 79 ++- ...ransaction_status_task_integration_test.go | 2 + ethereum/transaction_status_task_test.go | 119 +++++ identity/did.go | 43 +- identity/ideth/execute_integration_test.go | 13 +- identity/ideth/factory.go | 37 +- identity/ideth/factory_integration_test.go | 6 - identity/ideth/identity_contract.go | 104 ++-- identity/ideth/service.go | 118 +++-- identity/ideth/service_integration_test.go | 99 ++-- nft/ethereum_payment_obligation.go | 55 +-- nft/ethereum_payment_obligation_test.go | 2 +- nft/payment_obligation_integration_test.go | 40 +- p2p/client.go | 20 +- p2p/client_integration_test.go | 14 +- p2p/client_test.go | 71 +-- p2p/common/protocol.go | 10 + p2p/receiver/handler.go | 18 +- p2p/receiver/handler_integration_test.go | 186 +++++--- p2p/receiver/handler_test.go | 18 +- p2p/receiver/validator.go | 6 +- p2p/receiver/validator_test.go | 15 +- p2p/server.go | 24 +- p2p/server_test.go | 10 +- resources/data.go | 8 +- testingutils/anchors/mock_anchor_repo.go | 8 +- testingutils/commons/mock_did_identity.go | 31 +- testingutils/config/config.go | 2 +- testingutils/documents/documents.go | 12 +- testingutils/documents/invoice.go | 2 +- testingutils/documents/purchaseorder.go | 2 +- testingutils/identity/identity.go | 32 +- testingutils/setup.go | 9 +- testingutils/testingtx/mocktx.go | 4 + testworld/document_consensus_test.go | 53 +- testworld/httputils.go | 38 +- testworld/nft_test.go | 28 +- testworld/park.go | 19 +- testworld/park_test.go | 8 +- testworld/proof_test.go | 9 +- testworld/signature_test.go | 180 +++++++ testworld/start_test.go | 2 - transactions/transaction.go | 12 + transactions/txv1/base_task.go | 11 + transactions/txv1/manager.go | 16 +- transactions/txv1/manager_test.go | 8 +- utils/time.go | 13 +- utils/time_test.go | 5 +- 87 files changed, 3065 insertions(+), 957 deletions(-) create mode 100644 testworld/signature_test.go diff --git a/.travis.yml b/.travis.yml index 3b2a42a45..009fba8cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,13 +34,29 @@ install: jobs: include: - - stage: test + - stage: "Tests" + name: "Linting" script: - make lint-check proto-lint proto-all gen-swagger generate format-go - echo "Checking that prototool and format-go don't result in a modified git tree" && git diff --exit-code protobufs/gen - - ./build/scripts/test_wrapper.sh + - stage: "Tests" + name: "Unit and CMD tests" + script: + - ./build/scripts/test_wrapper.sh unit cmd + after_success: + - bash <(curl -s https://codecov.io/bash) + - stage: "Tests" + name: "Integration tests" + script: + - ./build/scripts/test_wrapper.sh integration + after_success: + - bash <(curl -s https://codecov.io/bash) + - stage: "Tests" + name: "Test world tests" + script: + - ./build/scripts/test_wrapper.sh testworld after_success: - - bash <(curl -s https://codecov.io/bash) + - bash <(curl -s https://codecov.io/bash) - stage: build_artifacts if: (NOT type IN (pull_request)) AND ((branch = develop) OR (branch = master)) before_script: diff --git a/Gopkg.lock b/Gopkg.lock index d55c31101..c0cf8da06 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -57,14 +57,14 @@ revision = "cff30e1d23fc9e800b2b5b4b41ef1817dda07e9f" [[projects]] - digest = "1:ead823956932d64e6271256398aa0753b03d4a6dec8f522847841ca33b98f289" + digest = "1:4dc9d52a48657d4eac97abd9743dd1f1ca1b00e93ff5c751cf0b9cb8e90dd41e" name = "github.com/centrifuge/centrifuge-ethereum-contracts" packages = ["."] pruneopts = "T" - revision = "be1a93f627115fcf9978a85c4b6b8f08adf87f35" + revision = "c5e55d16c4cfe058ca641d2a746a665f277f0c9f" [[projects]] - digest = "1:8e4b7179dfb51d02daeb1c3ee4bb816243837b74a6c1bd663a803776c43b8eaf" + digest = "1:bd19f674f48b13b3cd0336b0265893b2395f00557b9b10a29ce5df3456a998a8" name = "github.com/centrifuge/centrifuge-protobufs" packages = [ "documenttypes", @@ -76,14 +76,14 @@ "gen/go/purchaseorder", ] pruneopts = "T" - revision = "20c7fd5e4210e68d2a5662a8714d51ceeae031b8" + revision = "864a8ef4039324cebf3f23df115f50db12009d4c" [[projects]] - digest = "1:6c7200e9917373ebe3c248ca47f9ee8a7924aa003c137cbfee2c763d7bc0643f" + digest = "1:bdd797d675043d8547be2de04b25bcf7319ca834b72c88dd13f25972ac84e09a" name = "github.com/centrifuge/gocelery" packages = ["."] pruneopts = "UT" - revision = "fb11151a227ae41660e15f6c10e2e22eb1556531" + revision = "98e4381dac54051f9a6cd6d4e0c2815d87f625d8" [[projects]] digest = "1:3ddba8bdf84eff2bedcc8e262cb87370d1301cb8ca7a3503be29a6eae25bd037" diff --git a/Gopkg.toml b/Gopkg.toml index 563e9a89d..8ace970db 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,11 +28,11 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [[constraint]] name = "github.com/centrifuge/centrifuge-protobufs" - revision = "20c7fd5e4210e68d2a5662a8714d51ceeae031b8" + revision = "864a8ef4039324cebf3f23df115f50db12009d4c" [[override]] name = "github.com/centrifuge/centrifuge-ethereum-contracts" - revision = "be1a93f627115fcf9978a85c4b6b8f08adf87f35" + revision = "c5e55d16c4cfe058ca641d2a746a665f277f0c9f" [[constraint]] name = "github.com/Masterminds/semver" @@ -44,7 +44,7 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [[constraint]] name = "github.com/centrifuge/gocelery" - revision = "fb11151a227ae41660e15f6c10e2e22eb1556531" + revision = "98e4381dac54051f9a6cd6d4e0c2815d87f625d8" [[override]] name = "github.com/ethereum/go-ethereum" diff --git a/anchors/anchor_contract.go b/anchors/anchor_contract.go index 4eb11a880..a3bf29476 100644 --- a/anchors/anchor_contract.go +++ b/anchors/anchor_contract.go @@ -28,7 +28,7 @@ var ( ) // AnchorContractABI is the input ABI used to generate the binding from. -const AnchorContractABI = "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"anchorId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"documentRoot\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"blockHeight\",\"type\":\"uint32\"}],\"name\":\"AnchorCommitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"anchorId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"blockHeight\",\"type\":\"uint32\"}],\"name\":\"AnchorPreCommitted\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"signingRoot\",\"type\":\"bytes32\"}],\"name\":\"preCommit\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"anchorIdPreImage\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"},{\"name\":\"documentProofs\",\"type\":\"bytes32[]\"}],\"name\":\"commit\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"id\",\"type\":\"uint256\"}],\"name\":\"getAnchorById\",\"outputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"}],\"name\":\"hasValidPreCommit\",\"outputs\":[{\"name\":\"valid\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" +const AnchorContractABI = "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"anchorId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"documentRoot\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"blockHeight\",\"type\":\"uint32\"}],\"name\":\"AnchorCommitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"anchorId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"blockHeight\",\"type\":\"uint32\"}],\"name\":\"AnchorPreCommitted\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"signingRoot\",\"type\":\"bytes32\"}],\"name\":\"preCommit\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"anchorIdPreImage\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"},{\"name\":\"documentProofs\",\"type\":\"bytes32[]\"}],\"name\":\"commit\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"id\",\"type\":\"uint256\"}],\"name\":\"getAnchorById\",\"outputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"},{\"name\":\"blockNumber\",\"type\":\"uint32\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"anchorId\",\"type\":\"uint256\"}],\"name\":\"hasValidPreCommit\",\"outputs\":[{\"name\":\"valid\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" // AnchorContract is an auto generated Go binding around an Ethereum contract. type AnchorContract struct { @@ -174,14 +174,16 @@ func (_AnchorContract *AnchorContractTransactorRaw) Transact(opts *bind.Transact // GetAnchorById is a free data retrieval call binding the contract method 0x32bf361b. // -// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32) +// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32, blockNumber uint32) func (_AnchorContract *AnchorContractCaller) GetAnchorById(opts *bind.CallOpts, id *big.Int) (struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }, error) { ret := new(struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }) out := ret err := _AnchorContract.contract.Call(opts, out, "getAnchorById", id) @@ -190,20 +192,22 @@ func (_AnchorContract *AnchorContractCaller) GetAnchorById(opts *bind.CallOpts, // GetAnchorById is a free data retrieval call binding the contract method 0x32bf361b. // -// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32) +// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32, blockNumber uint32) func (_AnchorContract *AnchorContractSession) GetAnchorById(id *big.Int) (struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }, error) { return _AnchorContract.Contract.GetAnchorById(&_AnchorContract.CallOpts, id) } // GetAnchorById is a free data retrieval call binding the contract method 0x32bf361b. // -// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32) +// Solidity: function getAnchorById(id uint256) constant returns(anchorId uint256, documentRoot bytes32, blockNumber uint32) func (_AnchorContract *AnchorContractCallerSession) GetAnchorById(id *big.Int) (struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }, error) { return _AnchorContract.Contract.GetAnchorById(&_AnchorContract.CallOpts, id) } diff --git a/anchors/anchor_repository.go b/anchors/anchor_repository.go index 9f4472bdb..a6a078a9b 100644 --- a/anchors/anchor_repository.go +++ b/anchors/anchor_repository.go @@ -2,6 +2,7 @@ package anchors import ( "context" + "time" logging "github.com/ipfs/go-log" ) @@ -11,8 +12,16 @@ var log = logging.Logger("anchorRepository") // AnchorRepository defines a set of functions that can be // implemented by any type that stores and retrieves the anchoring, and pre anchoring details. type AnchorRepository interface { + + // PreCommitAnchor will call the transaction PreCommit on the smart contract, to pre commit a document update PreCommitAnchor(ctx context.Context, anchorID AnchorID, signingRoot DocumentRoot) (confirmations chan bool, err error) + + // CommitAnchor will send a commit transaction to Ethereum. CommitAnchor(ctx context.Context, anchorID AnchorID, documentRoot DocumentRoot, documentProofs [][32]byte) (chan bool, error) - GetDocumentRootOf(anchorID AnchorID) (DocumentRoot, error) + + // GetAnchorData takes an anchorID and returns the corresponding documentRoot from the chain. + GetAnchorData(anchorID AnchorID) (docRoot DocumentRoot, anchoredTime time.Time, err error) + + // HasValidPreCommit checks if the given anchorID has a valid pre-commit HasValidPreCommit(anchorID AnchorID) bool } diff --git a/anchors/anchor_repository_integration_test.go b/anchors/anchor_repository_integration_test.go index f8fe8d3f8..7178d5dae 100644 --- a/anchors/anchor_repository_integration_test.go +++ b/anchors/anchor_repository_integration_test.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "os" "testing" + "time" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" @@ -34,6 +35,7 @@ func TestMain(m *testing.M) { } func TestPreCommitAnchor_Integration(t *testing.T) { + t.Parallel() anchorID := utils.RandomSlice(32) signingRoot := utils.RandomSlice(32) @@ -45,6 +47,7 @@ func TestPreCommitAnchor_Integration(t *testing.T) { } func TestPreCommit_CommitAnchor_Integration(t *testing.T) { + t.Parallel() anchorIDPreImage := utils.RandomSlice(32) h := sha256.New() _, err := h.Write(anchorIDPreImage) @@ -80,12 +83,13 @@ func TestPreCommit_CommitAnchor_Integration(t *testing.T) { docRootTyped, _ := anchors.ToDocumentRoot(documentRoot) commitAnchor(t, anchorIDPreImage, documentRoot, [][anchors.DocumentProofLength]byte{proofB1, proofB2}) - gotDocRoot, err := anchorRepo.GetDocumentRootOf(anchorIDTyped) + gotDocRoot, _, err := anchorRepo.GetAnchorData(anchorIDTyped) assert.Nil(t, err) assert.Equal(t, docRootTyped, gotDocRoot) } func TestCommitAnchor_Integration(t *testing.T) { + t.Parallel() anchorIDPreImage := utils.RandomSlice(32) h := sha256.New() _, err := h.Write(anchorIDPreImage) @@ -98,9 +102,10 @@ func TestCommitAnchor_Integration(t *testing.T) { assert.NoError(t, err) docRootTyped, _ := anchors.ToDocumentRoot(documentRoot) commitAnchor(t, anchorIDPreImage, documentRoot, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}) - gotDocRoot, err := anchorRepo.GetDocumentRootOf(anchorIDTyped) + gotDocRoot, hval, err := anchorRepo.GetAnchorData(anchorIDTyped) assert.Nil(t, err) assert.Equal(t, docRootTyped, gotDocRoot) + assert.True(t, time.Now().After(hval)) } func commitAnchor(t *testing.T, anchorID, documentRoot []byte, documentProofs [][32]byte) { @@ -133,6 +138,7 @@ func preCommitAnchor(t *testing.T, anchorID, documentRoot []byte) { } func TestCommitAnchor_Integration_Concurrent(t *testing.T) { + t.Parallel() var commitDataList [5]*anchors.CommitData var doneList [5]chan bool @@ -165,7 +171,7 @@ func TestCommitAnchor_Integration_Concurrent(t *testing.T) { assert.True(t, isDone) anchorID := commitDataList[ix].AnchorID docRoot := commitDataList[ix].DocumentRoot - gotDocRoot, err := anchorRepo.GetDocumentRootOf(anchorID) + gotDocRoot, _, err := anchorRepo.GetAnchorData(anchorID) assert.Nil(t, err) assert.Equal(t, docRoot, gotDocRoot) } diff --git a/anchors/service.go b/anchors/service.go index cd9cd5fb3..f21fb048d 100644 --- a/anchors/service.go +++ b/anchors/service.go @@ -3,6 +3,7 @@ package anchors import ( "context" "math/big" + "time" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/ethereum" @@ -20,6 +21,7 @@ type anchorRepositoryContract interface { GetAnchorById(opts *bind.CallOpts, id *big.Int) (struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }, error) HasValidPreCommit(opts *bind.CallOpts, anchorId *big.Int) (bool, error) } @@ -47,12 +49,13 @@ func (s *service) HasValidPreCommit(anchorID AnchorID) bool { return r } -// GetDocumentRootOf takes an anchorID and returns the corresponding documentRoot from the chain. -func (s *service) GetDocumentRootOf(anchorID AnchorID) (docRoot DocumentRoot, err error) { +// GetAnchorData takes an anchorID and returns the corresponding documentRoot from the chain. +func (s *service) GetAnchorData(anchorID AnchorID) (docRoot DocumentRoot, anchoredTime time.Time, err error) { // Ignoring cancelFunc as code will block until response or timeout is triggered opts, _ := s.client.GetGethCallOpts(false) r, err := s.anchorRepositoryContract.GetAnchorById(opts, anchorID.BigInt()) - return r.DocumentRoot, err + blk, err := s.client.GetEthClient().BlockByNumber(context.Background(), big.NewInt(int64(r.BlockNumber))) + return r.DocumentRoot, time.Unix(blk.Time().Int64(), 0), err } // PreCommitAnchor will call the transaction PreCommit on the smart contract diff --git a/anchors/service_test.go b/anchors/service_test.go index bcb11758b..8f97c4955 100644 --- a/anchors/service_test.go +++ b/anchors/service_test.go @@ -8,7 +8,6 @@ import ( "github.com/centrifuge/go-centrifuge/crypto/secp256k1" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common/hexutil" @@ -24,11 +23,13 @@ type mockAnchorRepo struct { func (m *mockAnchorRepo) GetAnchorById(opts *bind.CallOpts, anchorID *big.Int) (struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 }, error) { args := m.Called(opts, anchorID) type Response struct { AnchorId *big.Int DocumentRoot [32]byte + BlockNumber uint32 } r := Response{} dr := args.Get(0).([32]byte) @@ -75,19 +76,3 @@ func TestGenerateAnchor(t *testing.T) { assert.Equal(t, commitData.DocumentProofs, documentProofs, "Anchor should have the document proofs") } - -func TestGetDocumentRootOf(t *testing.T) { - repo := &mockAnchorRepo{} - anchorID, err := ToAnchorID(utils.RandomSlice(32)) - assert.Nil(t, err) - - ethClient := &testingcommons.MockEthClient{} - ethClient.On("GetGethCallOpts").Return(nil) - ethRepo := newService(cfg, repo, nil, ethClient, nil) - docRoot := utils.RandomByte32() - repo.On("GetAnchorById", mock.Anything, mock.Anything).Return(docRoot, nil) - gotRoot, err := ethRepo.GetDocumentRootOf(anchorID) - repo.AssertExpectations(t) - assert.Nil(t, err) - assert.Equal(t, docRoot[:], gotRoot[:]) -} diff --git a/build/configs/default_config.yaml b/build/configs/default_config.yaml index 4f5f34c7d..00b4f95bd 100644 --- a/build/configs/default_config.yaml +++ b/build/configs/default_config.yaml @@ -26,9 +26,9 @@ networks: ethereumNetworkId: 4 # Latest deployed Smart Contracts for the given testnet contractAddresses: - identityFactory: "0xffe0612006eedaeda188cd9df54574a68920a97d" - anchorRepository: "0xd3be7846016367a08ad083b06df98ad78212d0a5" - paymentObligation: "0x2d5b6470af1ac962c55b6bee92f3c38c609f371e" + identityFactory: "0xb20f5ed00794c0cccc508b1d9fa882b631a3ff61" + anchorRepository: "0x2200d8c912551ccdcf960d302f318d3ece6d3959" + paymentObligation: "0x01ac3191b762e5072cc25ef5caede15a17840a89" # Kovan test network bernalheights: @@ -49,9 +49,9 @@ networks: ethereumNetworkId: 42 # Latest deployed Smart Contracts for the given testnet contractAddresses: - identityFactory: "0x01adb663afd3a5d7655c89d2774cef51800f3bb6" - anchorRepository: "0x16f45eac73752eab73072fddbacbd0a58adc3a40" - paymentObligation: "0xeb1c6a36c4b1234aaae7ece88fb4f48d23d63146" + identityFactory: "0x4c840990c5e96f4c4458486d44c68ed7e95e0d52" + anchorRepository: "0x625b95d4705d75c485d1773c7201a7343643e11f" + paymentObligation: "0x8fb2efb77b2d8d09a793bd313ebc830a7e7090b7" # Ropsten test network dogpatch: diff --git a/build/scripts/docker/entrypoint.sh b/build/scripts/docker/entrypoint.sh index f5834c4bc..76abd6fda 100755 --- a/build/scripts/docker/entrypoint.sh +++ b/build/scripts/docker/entrypoint.sh @@ -4,4 +4,7 @@ set -x CENT_MODE=${CENT_MODE:-run} -/root/centrifuge ${CENT_MODE} --config /root/.centrifuge/config/config.yaml $@ +ETHKEY=`cat /root/.centrifuge/secrets/eth.key` +ETHPWD=`cat /root/.centrifuge/secrets/eth.pwd` + +CENT_ETHEREUM_ACCOUNTS_MAIN_KEY=$ETHKEY CENT_ETHEREUM_ACCOUNTS_MAIN_PASSWORD=$ETHPWD /root/centrifuge ${CENT_MODE} --config /root/.centrifuge/config/config.yaml $@ diff --git a/build/scripts/migrate.sh b/build/scripts/migrate.sh index 997f479c8..2060cf820 100755 --- a/build/scripts/migrate.sh +++ b/build/scripts/migrate.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + # Allow passing parent directory as a parameter PARENT_DIR=$1 if [ -z ${PARENT_DIR} ]; diff --git a/build/scripts/test_wrapper.sh b/build/scripts/test_wrapper.sh index 03b27d3d2..4bb95a117 100755 --- a/build/scripts/test_wrapper.sh +++ b/build/scripts/test_wrapper.sh @@ -35,14 +35,24 @@ status=$? ############################################################ ################# Run Tests ################################ -if [ $status -eq 0 ]; then +args=( "$@" ) +if [[ $# == 0 ]]; then + args=( unit cmd testworld integration ) +fi + +if [[ ${status} -eq 0 ]]; then statusAux=0 for path in ${local_dir}/tests/*; do - [ -x "${path}" ] || continue # if not an executable, skip + [[ -x "${path}" ]] || continue # if not an executable, skip - echo "Executing test suite [${path}]" - ./$path - statusAux="$(( $statusAux | $? ))" + for arg in "${args[@]}"; do + if [[ ${path} == *$arg* ]]; then + echo "Executing test suite [${path}]" + ./$path + statusAux="$(( $statusAux | $? ))" + continue + fi + done done # Store status of tests status=$statusAux diff --git a/config/configstore/model.go b/config/configstore/model.go index dffae8d0c..8be6a058e 100644 --- a/config/configstore/model.go +++ b/config/configstore/model.go @@ -538,7 +538,8 @@ func (acc *Account) SignMsg(msg []byte) (*coredocumentpb.Signature, error) { if err != nil { return nil, err } - signature, err := crypto.SignMessage(keys[identity.KeyPurposeSigning.Name].PrivateKey, msg, crypto.CurveSecp256K1) + signingKeyPair := keys[identity.KeyPurposeSigning.Name] + signature, err := crypto.SignMessage(signingKeyPair.PrivateKey, msg, crypto.CurveSecp256K1) if err != nil { return nil, err } @@ -549,10 +550,10 @@ func (acc *Account) SignMsg(msg []byte) (*coredocumentpb.Signature, error) { } return &coredocumentpb.Signature{ - EntityId: did, - PublicKey: keys[identity.KeyPurposeSigning.Name].PublicKey, - Signature: signature, - Timestamp: utils.ToTimestamp(time.Now().UTC()), + SignatureId: append(did, signingKeyPair.PublicKey...), + SignerId: did, + PublicKey: signingKeyPair.PublicKey, + Signature: signature, }, nil } diff --git a/contextutil/context.go b/contextutil/context.go index 0c87f0e6e..f159c9545 100644 --- a/contextutil/context.go +++ b/contextutil/context.go @@ -56,7 +56,7 @@ func AccountDID(ctx context.Context) (identity.DID, error) { return identity.NewDIDFromBytes(didBytes), nil } -// Account extracts the TenanConfig from the given context value +// Account extracts the TenantConfig from the given context value func Account(ctx context.Context) (config.Account, error) { tc, ok := ctx.Value(self).(config.Account) if !ok { diff --git a/crypto/keytools.go b/crypto/keytools.go index 45f2b11c2..e231f028f 100644 --- a/crypto/keytools.go +++ b/crypto/keytools.go @@ -1,7 +1,10 @@ package crypto import ( + "github.com/centrifuge/go-centrifuge/crypto/ed25519" + "github.com/centrifuge/go-centrifuge/errors" logging "github.com/ipfs/go-log" + "github.com/libp2p/go-libp2p-crypto" ) var log = logging.Logger("keytools") @@ -11,3 +14,24 @@ const ( CurveEd25519 string = "ed25519" CurveSecp256K1 string = "secp256k1" ) + +// ObtainP2PKeypair obtains a key pair from given file paths +func ObtainP2PKeypair(pubKeyFile, privKeyFile string) (priv crypto.PrivKey, pub crypto.PubKey, err error) { + // Create the signing key for the host + publicKey, privateKey, err := ed25519.GetSigningKeyPair(pubKeyFile, privKeyFile) + if err != nil { + return nil, nil, errors.New("failed to get keys: %v", err) + } + + var key []byte + key = append(key, privateKey...) + key = append(key, publicKey...) + + priv, err = crypto.UnmarshalEd25519PrivateKey(key) + if err != nil { + return nil, nil, err + } + + pub = priv.GetPublic() + return priv, pub, nil +} diff --git a/documents/anchor_task.go b/documents/anchor_task.go index f96f2cf5a..a63130123 100644 --- a/documents/anchor_task.go +++ b/documents/anchor_task.go @@ -100,7 +100,8 @@ func (d *documentAnchorTask) RunTask() (res interface{}, err error) { apiLog.Error(err) return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get header: %v", err)) } - ctxh, err := contextutil.New(context.Background(), tc) + txctx := contextutil.WithTX(context.Background(), d.TxID) + ctxh, err := contextutil.New(txctx, tc) if err != nil { return false, errors.New("failed to get context header: %v", err) } diff --git a/documents/bootstrapper.go b/documents/bootstrapper.go index 8ad66bc6f..e3eb9b0ef 100644 --- a/documents/bootstrapper.go +++ b/documents/bootstrapper.go @@ -21,6 +21,9 @@ const ( // BootstrappedDocumentService is the key to bootstrapped document service BootstrappedDocumentService = "BootstrappedDocumentService" + + // BootstrappedAnchorProcessor is the key to bootstrapped anchor processor + BootstrappedAnchorProcessor = "BootstrappedAnchorProcessor" ) // Bootstrapper implements bootstrap.Bootstrapper. @@ -63,11 +66,6 @@ func (PostBootstrapper) Bootstrap(ctx map[string]interface{}) error { return errors.New("config service not initialised") } - cfg, ok := ctx[bootstrap.BootstrappedConfig].(Config) - if !ok { - return errors.New("documents config not initialised") - } - queueSrv, ok := ctx[bootstrap.BootstrappedQueueServer].(*queue.Server) if !ok { return errors.New("queue not initialised") @@ -78,14 +76,14 @@ func (PostBootstrapper) Bootstrap(ctx map[string]interface{}) error { return errors.New("document repository not initialised") } - didService, ok := ctx[identity.BootstrappedDIDService].(identity.ServiceDID) + anchorRepo, ok := ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) if !ok { - return errors.New("identity service not initialized") + return errors.New("anchor repository not initialised") } - anchorRepo, ok := ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) + cfg, ok := ctx[bootstrap.BootstrappedConfig].(Config) if !ok { - return errors.New("anchor repository not initialised") + return errors.New("documents config not initialised") } p2pClient, ok := ctx[bootstrap.BootstrappedPeer].(Client) @@ -93,13 +91,21 @@ func (PostBootstrapper) Bootstrap(ctx map[string]interface{}) error { return errors.New("p2p client not initialised") } + didService, ok := ctx[identity.BootstrappedDIDService].(identity.ServiceDID) + if !ok { + return errors.New("identity service not initialized") + } + + dp := DefaultProcessor(didService, p2pClient, anchorRepo, cfg) + ctx[BootstrappedAnchorProcessor] = dp + txMan := ctx[transactions.BootstrappedService].(transactions.Manager) anchorTask := &documentAnchorTask{ BaseTask: txv1.BaseTask{ TxManager: txMan, }, config: cfgService, - processor: DefaultProcessor(didService, p2pClient, anchorRepo, cfg), + processor: dp, modelGetFunc: repo.Get, modelSaveFunc: repo.Update, } diff --git a/documents/coredocument.go b/documents/coredocument.go index 1c2b38dd8..ecaafe921 100644 --- a/documents/coredocument.go +++ b/documents/coredocument.go @@ -2,9 +2,9 @@ package documents import ( "crypto/sha256" - "encoding/binary" "fmt" "strings" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/crypto" @@ -26,8 +26,8 @@ const ( // DocumentTypeField represents the doc type property of a tree DocumentTypeField = "document_type" - // SignaturesField represents the signatures property of a tree - SignaturesField = "signatures" + // SignaturesRootField represents the signatures property of a tree + SignaturesRootField = "signatures_root" // SigningRootField represents the signature root property of a tree SigningRootField = "signing_root" @@ -38,24 +38,32 @@ const ( // nftByteCount is the length of combined bytes of registry and tokenID nftByteCount = 52 + // DRTreePrefix is the human readable prefix for core doc tree props + DRTreePrefix = "dr_tree" + // CDTreePrefix is the human readable prefix for core doc tree props CDTreePrefix = "cd_tree" // SigningTreePrefix is the human readable prefix for signing tree props SigningTreePrefix = "signing_tree" + + // SignaturesTreePrefix is the human readable prefix for signature props + SignaturesTreePrefix = "signatures_tree" ) func compactProperties(key string) []byte { m := map[string][]byte{ - CDRootField: {0, 0, 0, 7}, - DataRootField: {0, 0, 0, 5}, - DocumentTypeField: {0, 0, 0, 100}, - SignaturesField: {0, 0, 0, 6}, - SigningRootField: {0, 0, 0, 10}, + CDRootField: {0, 0, 0, 7}, + DataRootField: {0, 0, 0, 5}, + DocumentTypeField: {0, 0, 0, 100}, + SignaturesRootField: {0, 0, 0, 6}, + SigningRootField: {0, 0, 0, 10}, // tree prefixes use the first byte of a 4 byte slice by convention - CDTreePrefix: {1, 0, 0, 0}, - SigningTreePrefix: {2, 0, 0, 0}, + CDTreePrefix: {1, 0, 0, 0}, + SigningTreePrefix: {2, 0, 0, 0}, + SignaturesTreePrefix: {3, 0, 0, 0}, + DRTreePrefix: {4, 0, 0, 0}, } return m[key] } @@ -67,7 +75,9 @@ type CoreDocument struct { // newCoreDocument returns a new CoreDocument. func newCoreDocument() (*CoreDocument, error) { - cd := coredocumentpb.CoreDocument{} + cd := coredocumentpb.CoreDocument{ + SignatureData: new(coredocumentpb.SignatureData), + } err := populateVersions(&cd, nil) if err != nil { return nil, err @@ -83,8 +93,9 @@ func NewCoreDocumentFromProtobuf(cd coredocumentpb.CoreDocument) *CoreDocument { return &CoreDocument{Document: cd} } -// NewCoreDocumentWithCollaborators generates new core Document, adds collaborators, adds read rules and fills salts -func NewCoreDocumentWithCollaborators(collaborators []string) (*CoreDocument, error) { +// NewCoreDocumentWithCollaborators generates new core Document with a document type specified by the prefix: po or invoice. +// It then adds collaborators, adds read rules and fills salts. +func NewCoreDocumentWithCollaborators(collaborators []string, documentPrefix []byte) (*CoreDocument, error) { cd, err := newCoreDocument() if err != nil { return nil, errors.New("failed to create coredoc: %v", err) @@ -96,6 +107,7 @@ func NewCoreDocumentWithCollaborators(collaborators []string) (*CoreDocument, er } cd.initReadRules(ids) + cd.initTransitionRules(ids, documentPrefix) if err := cd.setSalts(); err != nil { return nil, err } @@ -135,7 +147,10 @@ func (cd *CoreDocument) PreviousDocumentRoot() []byte { // AppendSignatures appends signatures to core Document. func (cd *CoreDocument) AppendSignatures(signs ...*coredocumentpb.Signature) { - cd.Document.Signatures = append(cd.Document.Signatures, signs...) + if cd.Document.SignatureData == nil { + cd.Document.SignatureData = new(coredocumentpb.SignatureData) + } + cd.Document.SignatureData.Signatures = append(cd.Document.SignatureData.Signatures, signs...) } // setSalts generate salts for core Document. @@ -156,7 +171,7 @@ func (cd *CoreDocument) setSalts() error { // PrepareNewVersion prepares the next version of the CoreDocument // if initSalts is true, salts will be generated for new version. -func (cd *CoreDocument) PrepareNewVersion(collaborators []string, initSalts bool) (*CoreDocument, error) { +func (cd *CoreDocument) PrepareNewVersion(collaborators []string, initSalts bool, documentPrefix []byte) (*CoreDocument, error) { if len(cd.Document.DocumentRoot) != idSize { return nil, errors.New("Document root is invalid") } @@ -181,6 +196,7 @@ func (cd *CoreDocument) PrepareNewVersion(collaborators []string, initSalts bool TransitionRules: cd.Document.TransitionRules, Nfts: cd.Document.Nfts, AccessTokens: cd.Document.AccessTokens, + SignatureData: new(coredocumentpb.SignatureData), } err = populateVersions(&cdp, &cd.Document) @@ -190,6 +206,7 @@ func (cd *CoreDocument) PrepareNewVersion(collaborators []string, initSalts bool ncd := &CoreDocument{Document: cdp} ncd.addCollaboratorsToReadSignRules(ucs) + ncd.addCollaboratorsToTransitionRules(ucs, documentPrefix) if !initSalts { return ncd, nil @@ -203,97 +220,166 @@ func (cd *CoreDocument) PrepareNewVersion(collaborators []string, initSalts bool return ncd, nil } -// addCollaboratorsToReadSignRules adds the given collaborators to a new read rule with READ_SIGN capability. -// The operation is no-op if no collaborators is provided. -// The operation is not idempotent. So calling twice with same accounts will lead to read rules duplication. -func (cd *CoreDocument) addCollaboratorsToReadSignRules(collaborators []identity.DID) { +// newRole returns a new role with random role key +func newRole() *coredocumentpb.Role { + return &coredocumentpb.Role{RoleKey: utils.RandomSlice(idSize)} +} + +// newRoleWithCollaborators creates a new Role and adds the given collaborators to this Role. +// The Role is then returned. +// The operation returns a nil Role if no collaborators are provided. +func newRoleWithCollaborators(collaborators []identity.DID) *coredocumentpb.Role { if len(collaborators) == 0 { - return + return nil } // create a role for given collaborators - role := new(coredocumentpb.Role) - role.RoleKey = utils.RandomSlice(idSize) + role := newRole() for _, c := range collaborators { c := c role.Collaborators = append(role.Collaborators, c[:]) } + return role +} - cd.addNewRule(role, coredocumentpb.Action_ACTION_READ_SIGN) +// TreeProof is a helper structure to pass to create proofs +type TreeProof struct { + tree *proofs.DocumentTree + treeHashes [][]byte } -// addNewRule creates a new rule as per the role and action. -func (cd *CoreDocument) addNewRule(role *coredocumentpb.Role, action coredocumentpb.Action) { - cd.Document.Roles = append(cd.Document.Roles, role) - rule := new(coredocumentpb.ReadRule) - rule.Roles = append(rule.Roles, role.RoleKey) - rule.Action = action - cd.Document.ReadRules = append(cd.Document.ReadRules, rule) +// newTreeProof returns a TreeProof instance pointer +func newTreeProof(t *proofs.DocumentTree, th [][]byte) *TreeProof { + return &TreeProof{tree: t, treeHashes: th} } // CreateProofs takes Document data tree and list to fields and generates proofs. // we will try generating proofs from the dataTree. If failed, we will generate proofs from CoreDocument. // errors out when the proof generation is failed on core Document tree. -func (cd *CoreDocument) CreateProofs(docType string, dataTree *proofs.DocumentTree, fields []string) (proofs []*proofspb.Proof, err error) { - srpHashes, err := cd.GetSigningRootProof() +func (cd *CoreDocument) CreateProofs(docType string, dataTree *proofs.DocumentTree, fields []string) (prfs []*proofspb.Proof, err error) { + treeProofs := make(map[string]*TreeProof, 3) + + drTree, err := cd.DocumentRootTree() if err != nil { - return nil, errors.New("failed to generate signing root proofs: %v", err) + return nil, err + } + signatureTree, err := cd.getSignatureDataTree() + if err != nil { + return nil, errors.New("failed to generate signatures tree: %v", err) } - cdTree, err := cd.documentTree(docType) if err != nil { return nil, errors.New("failed to generate core Document tree: %v", err) } + srHash, err := cd.GetSigningRootHash() + if err != nil { + return nil, errors.New("failed to generate signing root proofs: %v", err) + } dataRoot := dataTree.RootHash() cdRoot := cdTree.RootHash() - // try generating proofs from data root first - proofs, missedPfs := generateProofs(dataTree, fields, append([][]byte{cdRoot}, srpHashes...)) - if len(missedPfs) == 0 { - return proofs, nil + dataPrefix, err := getDataTreePrefix(dataTree) + if err != nil { + return nil, err } - // generate proofs from cdTree. fail if any proofs are missed after this - pfs, missedPfs := generateProofs(cdTree, missedPfs, append([][]byte{dataRoot}, srpHashes...)) - if len(missedPfs) > 0 { - return nil, errors.New("failed to generate proofs for %v", missedPfs) - } + treeProofs[DRTreePrefix] = newTreeProof(drTree, nil) + treeProofs[dataPrefix] = newTreeProof(dataTree, append([][]byte{cdRoot}, signatureTree.RootHash())) + treeProofs[SignaturesTreePrefix] = newTreeProof(signatureTree, [][]byte{srHash}) + treeProofs[CDTreePrefix] = newTreeProof(cdTree, append([][]byte{dataRoot}, signatureTree.RootHash())) + + return generateProofs(fields, treeProofs) +} - proofs = append(proofs, pfs...) - return proofs, nil +// TODO remove as soon as we have a public method that retrieves the parent prefix +func getDataTreePrefix(dataTree *proofs.DocumentTree) (string, error) { + props := dataTree.PropertyOrder() + if len(props) == 0 { + return "", errors.New("no properties found in data tree") + } + fidx := strings.Split(props[0].ReadableName(), ".") + if len(fidx) == 1 { + return "", errors.New("no prefix found in data tree property") + } + return fidx[0], nil } -func generateProofs(tree *proofs.DocumentTree, fields []string, appendHashes [][]byte) (proofs []*proofspb.Proof, missedProofs []string) { +// generateProofs creates proofs from fields and trees and hashes provided +func generateProofs(fields []string, treeProofs map[string]*TreeProof) (prfs []*proofspb.Proof, err error) { for _, f := range fields { + fidx := strings.Split(f, ".") + t, ok := treeProofs[fidx[0]] + if !ok { + return nil, errors.New("failed to find prefix tree in supported list") + } + tree := t.tree proof, err := tree.CreateProof(f) if err != nil { - // add the missed proof to the map - missedProofs = append(missedProofs, f) - continue + return nil, err } - - proof.SortedHashes = append(proof.SortedHashes, appendHashes...) - proofs = append(proofs, &proof) + thashes := treeProofs[fidx[0]].treeHashes + proof.SortedHashes = append(proof.SortedHashes, thashes...) + prfs = append(prfs, &proof) } - - return proofs, missedProofs + return prfs, nil } -// GetSigningRootProof returns the hashes needed to create a proof for fields from SigningRoot to DocumentRoot. -// The returned proofs are appended to the proofs generated from the data tree and core Document tree for a successful verification. -func (cd *CoreDocument) GetSigningRootProof() (hashes [][]byte, err error) { +// GetSigningRootHash returns the hash needed to create a proof for fields from SigningRoot to DocumentRoot. +// The returned proof is appended to the proofs generated from the data tree and core Document tree for a successful verification. +func (cd *CoreDocument) GetSigningRootHash() (hash []byte, err error) { tree, err := cd.DocumentRootTree() if err != nil { return } - rootProof, err := tree.CreateProof("signing_root") + rootProof, err := tree.CreateProof(fmt.Sprintf("%s.%s", DRTreePrefix, SigningRootField)) if err != nil { return } + return rootProof.Hash, err +} + +// GetSignaturesRootHash returns the hash needed to create proofs from SignaturesRoot to DocumentRoot +func (cd *CoreDocument) GetSignaturesRootHash() (hash []byte, err error) { + tree, err := cd.getSignatureDataTree() + if err != nil { + return + } + return tree.RootHash(), nil +} + +// setSignatureDataSalts generate salts for SignatureData. +// This is no-op if the salts are already generated. +func (cd *CoreDocument) setSignatureDataSalts() ([]*coredocumentpb.DocumentSalt, error) { + if cd.Document.SignatureDataSalts == nil { + proofSalts, err := GenerateNewSalts(cd.Document.SignatureData, SignaturesTreePrefix, compactProperties(SignaturesTreePrefix)) + if err != nil { + return nil, err + } + cd.Document.SignatureDataSalts = ConvertToProtoSalts(proofSalts) + } + return cd.Document.SignatureDataSalts, nil +} + +// getSignatureDataTree returns the merkle tree for the Signature Data root. +func (cd *CoreDocument) getSignatureDataTree() (*proofs.DocumentTree, error) { + signatureSalts, err := cd.setSignatureDataSalts() + if err != nil { + return nil, err + } + tree := NewDefaultTreeWithPrefix(ConvertToProofSalts(signatureSalts), SignaturesTreePrefix, compactProperties(SignaturesTreePrefix)) + + err = tree.AddLeavesFromDocument(cd.Document.SignatureData) + if err != nil { + return nil, err + } - return rootProof.SortedHashes, err + err = tree.Generate() + if err != nil { + return nil, err + } + return tree, nil } // DocumentRootTree returns the merkle tree for the Document root. @@ -302,49 +388,26 @@ func (cd *CoreDocument) DocumentRootTree() (tree *proofs.DocumentTree, err error return nil, errors.New("signing root is invalid") } - tree = NewDefaultTree(ConvertToProofSalts(cd.Document.CoredocumentSalts)) + tree = NewDefaultTreeWithPrefix(ConvertToProofSalts(cd.Document.CoredocumentSalts), DRTreePrefix, compactProperties(DRTreePrefix)) // The first leave added is the signing_root err = tree.AddLeaf(proofs.LeafNode{ Hash: cd.Document.SigningRoot, Hashed: true, - Property: NewLeafProperty(SigningRootField, compactProperties(SigningRootField))}) + Property: NewLeafProperty(fmt.Sprintf("%s.%s", DRTreePrefix, SigningRootField), append(compactProperties(DRTreePrefix), compactProperties(SigningRootField)...))}) if err != nil { return nil, err } - // For every signature we create a LeafNode - sigProperty := NewLeafProperty(SignaturesField, compactProperties(SignaturesField)) - sigLeafList := make([]proofs.LeafNode, len(cd.Document.Signatures)+1) - sigLengthNode := proofs.LeafNode{ - Property: sigProperty.LengthProp(proofs.DefaultSaltsLengthSuffix), - Salt: make([]byte, idSize), - Value: []byte(fmt.Sprintf("%d", len(cd.Document.Signatures))), - } - h := sha256.New() - err = sigLengthNode.HashNode(h, true) + // Second leaf from the signature data tree + signatureTree, err := cd.getSignatureDataTree() if err != nil { return nil, err } - - sigLeafList[0] = sigLengthNode - for i, sig := range cd.Document.Signatures { - payload := sha256.Sum256(append(sig.EntityId, append(sig.PublicKey, sig.Signature...)...)) - leaf := proofs.LeafNode{ - Hash: payload[:], - Hashed: true, - Property: sigProperty.SliceElemProp(proofs.FieldNumForSliceLength(i)), - } - - err = leaf.HashNode(h, true) - if err != nil { - return nil, err - } - - sigLeafList[i+1] = leaf - } - - err = tree.AddLeaves(sigLeafList) + err = tree.AddLeaf(proofs.LeafNode{ + Hash: signatureTree.RootHash(), + Hashed: true, + Property: NewLeafProperty(fmt.Sprintf("%s.%s", DRTreePrefix, SignaturesRootField), append(compactProperties(DRTreePrefix), compactProperties(SignaturesRootField)...))}) if err != nil { return nil, err } @@ -370,16 +433,14 @@ func (cd *CoreDocument) signingRootTree(docType string) (tree *proofs.DocumentTr // create the signing tree with data root and coredoc root as siblings tree = NewDefaultTreeWithPrefix(ConvertToProofSalts(cd.Document.CoredocumentSalts), SigningTreePrefix, compactProperties(SigningTreePrefix)) - prefixProp := NewLeafProperty(SigningTreePrefix, compactProperties(SigningTreePrefix)) - err = tree.AddLeaves([]proofs.LeafNode{ { - Property: prefixProp.FieldProp(DataRootField, binary.LittleEndian.Uint32(compactProperties(DataRootField))), + Property: NewLeafProperty(fmt.Sprintf("%s.%s", SigningTreePrefix, DataRootField), append(compactProperties(SigningTreePrefix), compactProperties(DataRootField)...)), Hash: cd.Document.DataRoot, Hashed: true, }, { - Property: prefixProp.FieldProp(CDRootField, binary.LittleEndian.Uint32(compactProperties(CDRootField))), + Property: NewLeafProperty(fmt.Sprintf("%s.%s", SigningTreePrefix, CDRootField), append(compactProperties(SigningTreePrefix), compactProperties(CDRootField)...)), Hash: cdTree.RootHash(), Hashed: true, }, @@ -405,10 +466,10 @@ func (cd *CoreDocument) documentTree(docType string) (tree *proofs.DocumentTree, return nil, err } - prefixProp := NewLeafProperty(CDTreePrefix, compactProperties(CDTreePrefix)) + dtProp := NewLeafProperty(fmt.Sprintf("%s.%s", CDTreePrefix, DocumentTypeField), append(compactProperties(CDTreePrefix), compactProperties(DocumentTypeField)...)) // Adding document type as it is an excluded field in the tree documentTypeNode := proofs.LeafNode{ - Property: prefixProp.FieldProp(DocumentTypeField, binary.LittleEndian.Uint32(compactProperties(DocumentTypeField))), + Property: dtProp, Salt: make([]byte, 32), Value: []byte(docType), } @@ -534,13 +595,33 @@ func (cd *CoreDocument) PackCoreDocument(data *any.Any, salts []*coredocumentpb. // Signatures returns the copy of the signatures on the Document. func (cd *CoreDocument) Signatures() (signatures []coredocumentpb.Signature) { - for _, s := range cd.Document.Signatures { + for _, s := range cd.Document.SignatureData.Signatures { signatures = append(signatures, *s) } return signatures } +// AddUpdateLog adds a log to the model to persist an update related meta data such as author +func (cd *CoreDocument) AddUpdateLog(account identity.DID) (err error) { + cd.Document.Author = account[:] + cd.Document.Timestamp, err = utils.ToTimestamp(time.Now().UTC()) + if err != nil { + return err + } + return nil +} + +// Author is the author of the document version represented by the model +func (cd *CoreDocument) Author() identity.DID { + return identity.NewDIDFromBytes(cd.Document.Author) +} + +// Timestamp is the time of update in UTC of the document version represented by the model +func (cd *CoreDocument) Timestamp() (time.Time, error) { + return utils.FromTimestamp(cd.Document.Timestamp) +} + func populateVersions(cd *coredocumentpb.CoreDocument, prevCD *coredocumentpb.CoreDocument) (err error) { if prevCD != nil { cd.PreviousVersion = prevCD.CurrentVersion diff --git a/documents/coredocument_test.go b/documents/coredocument_test.go index a2f3c81e3..a53f857bf 100644 --- a/documents/coredocument_test.go +++ b/documents/coredocument_test.go @@ -4,6 +4,7 @@ package documents import ( "crypto/sha256" + "fmt" "os" "testing" @@ -24,6 +25,7 @@ import ( "github.com/centrifuge/go-centrifuge/transactions/txv1" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes/any" "github.com/stretchr/testify/assert" ) @@ -119,7 +121,7 @@ func TestCoreDocument_PrepareNewVersion(t *testing.T) { c1 := testingidentity.GenerateRandomDID() c2 := testingidentity.GenerateRandomDID() c := []string{c1.String(), c2.String()} - ncd, err := cd.PrepareNewVersion(c, false) + ncd, err := cd.PrepareNewVersion(c, false, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Document root is invalid") assert.Nil(t, ncd) @@ -127,13 +129,13 @@ func TestCoreDocument_PrepareNewVersion(t *testing.T) { //collaborators need to be hex string cd.Document.DocumentRoot = utils.RandomSlice(32) collabs := []string{"some ID"} - ncd, err = cd.PrepareNewVersion(collabs, false) + ncd, err = cd.PrepareNewVersion(collabs, false, nil) assert.Error(t, err) assert.True(t, errors.IsOfType(identity.ErrMalformedAddress, err)) assert.Nil(t, ncd) // successful preparation of new version upon addition of DocumentRoot - ncd, err = cd.PrepareNewVersion(c, false) + ncd, err = cd.PrepareNewVersion(c, false, nil) assert.NoError(t, err) assert.NotNil(t, ncd) cs, err := ncd.GetCollaborators() @@ -148,7 +150,7 @@ func TestCoreDocument_PrepareNewVersion(t *testing.T) { expectedNextVersion = h.Sum(expectedNextVersion) assert.Equal(t, expectedNextVersion, ncd.Document.NextVersion) - ncd, err = cd.PrepareNewVersion(c, true) + ncd, err = cd.PrepareNewVersion(c, true, nil) assert.NoError(t, err) assert.NotNil(t, ncd) cs, err = ncd.GetCollaborators() @@ -164,14 +166,19 @@ func TestCoreDocument_PrepareNewVersion(t *testing.T) { assert.Equal(t, cd.Document.DocumentRoot, ncd.Document.PreviousRoot) assert.Len(t, cd.Document.Roles, 0) assert.Len(t, cd.Document.ReadRules, 0) - assert.Len(t, ncd.Document.Roles, 1) + assert.Len(t, cd.Document.TransitionRules, 0) + assert.Len(t, ncd.Document.Roles, 2) assert.Len(t, ncd.Document.ReadRules, 1) + assert.Len(t, ncd.Document.TransitionRules, 2) assert.Len(t, ncd.Document.Roles[0].Collaborators, 2) assert.Equal(t, ncd.Document.Roles[0].Collaborators[0], c1[:]) assert.Equal(t, ncd.Document.Roles[0].Collaborators[1], c2[:]) + assert.Len(t, ncd.Document.Roles[1].Collaborators, 2) + assert.Equal(t, ncd.Document.Roles[1].Collaborators[0], c1[:]) + assert.Equal(t, ncd.Document.Roles[1].Collaborators[1], c2[:]) } -func TestGetSigningProofHashes(t *testing.T) { +func TestGetSigningProofHash(t *testing.T) { docAny := &any.Any{ TypeUrl: documenttypes.InvoiceDataTypeUrl, Value: []byte{}, @@ -190,15 +197,52 @@ func TestGetSigningProofHashes(t *testing.T) { _, err = cd.CalculateDocumentRoot() assert.Nil(t, err) - hashes, err := cd.GetSigningRootProof() + signatureTree, err := cd.getSignatureDataTree() assert.Nil(t, err) - assert.Equal(t, 1, len(hashes)) - valid, err := proofs.ValidateProofSortedHashes(cd.Document.SigningRoot, hashes, cd.Document.DocumentRoot, sha256.New()) + valid, err := proofs.ValidateProofSortedHashes(cd.Document.SigningRoot, [][]byte{signatureTree.RootHash()}, cd.Document.DocumentRoot, sha256.New()) assert.True(t, valid) assert.Nil(t, err) } +func TestGetSignaturesTree(t *testing.T) { + docAny := &any.Any{ + TypeUrl: documenttypes.InvoiceDataTypeUrl, + Value: []byte{}, + } + + cd, err := newCoreDocument() + assert.NoError(t, err) + cd.Document.EmbeddedData = docAny + cd.Document.DataRoot = utils.RandomSlice(32) + sig := &coredocumentpb.Signature{ + SignerId: utils.RandomSlice(identity.DIDLength), + PublicKey: utils.RandomSlice(32), + SignatureId: utils.RandomSlice(52), + Signature: utils.RandomSlice(32), + } + cd.Document.SignatureData.Signatures = []*coredocumentpb.Signature{sig} + err = cd.setSalts() + assert.NoError(t, err) + + signatureTree, err := cd.getSignatureDataTree() + assert.NoError(t, err) + assert.NotNil(t, signatureTree) + + lengthIdx, lengthLeaf := signatureTree.GetLeafByProperty(SignaturesTreePrefix + ".signatures.length") + assert.Equal(t, 0, lengthIdx) + assert.NotNil(t, lengthLeaf) + assert.Equal(t, SignaturesTreePrefix+".signatures.length", lengthLeaf.Property.ReadableName()) + assert.Equal(t, append(compactProperties(SignaturesTreePrefix), []byte{0, 0, 0, 1}...), lengthLeaf.Property.CompactName()) + + signerKey := hexutil.Encode(sig.SignatureId) + _, signerLeaf := signatureTree.GetLeafByProperty(fmt.Sprintf("%s.signatures[%s].signer_id", SignaturesTreePrefix, signerKey)) + assert.NotNil(t, signerLeaf) + assert.Equal(t, fmt.Sprintf("%s.signatures[%s].signer_id", SignaturesTreePrefix, signerKey), signerLeaf.Property.ReadableName()) + assert.Equal(t, append(compactProperties(SignaturesTreePrefix), append([]byte{0, 0, 0, 1}, append(sig.SignatureId, []byte{0, 0, 0, 2}...)...)...), signerLeaf.Property.CompactName()) + assert.Equal(t, sig.SignerId, signerLeaf.Value) +} + func TestGetDocumentSigningTree(t *testing.T) { cd, err := newCoreDocument() assert.NoError(t, err) @@ -227,6 +271,14 @@ func TestGetDocumentRootTree(t *testing.T) { cd, err := newCoreDocument() assert.NoError(t, err) + sig := &coredocumentpb.Signature{ + SignerId: utils.RandomSlice(identity.DIDLength), + PublicKey: utils.RandomSlice(32), + SignatureId: utils.RandomSlice(52), + Signature: utils.RandomSlice(32), + } + cd.Document.SignatureData.Signatures = []*coredocumentpb.Signature{sig} + // no signing root generated _, err = cd.DocumentRootTree() assert.Error(t, err) @@ -235,15 +287,21 @@ func TestGetDocumentRootTree(t *testing.T) { cd.Document.SigningRoot = utils.RandomSlice(32) tree, err := cd.DocumentRootTree() assert.NoError(t, err) - _, leaf := tree.GetLeafByProperty("signing_root") + _, leaf := tree.GetLeafByProperty(fmt.Sprintf("%s.%s", DRTreePrefix, SigningRootField)) assert.NotNil(t, leaf) assert.Equal(t, cd.Document.SigningRoot, leaf.Hash) + + // Get signaturesLeaf + _, signaturesLeaf := tree.GetLeafByProperty(fmt.Sprintf("%s.%s", DRTreePrefix, SignaturesRootField)) + assert.NotNil(t, signaturesLeaf) + assert.Equal(t, fmt.Sprintf("%s.%s", DRTreePrefix, SignaturesRootField), signaturesLeaf.Property.ReadableName()) + assert.Equal(t, append(compactProperties(DRTreePrefix), compactProperties(SignaturesRootField)...), signaturesLeaf.Property.CompactName()) } func TestCoreDocument_GenerateProofs(t *testing.T) { h := sha256.New() - testTree := NewDefaultTree(nil) - props := []proofs.Property{NewLeafProperty("sample_field", []byte{0, 0, 0, 200}), NewLeafProperty("sample_field2", []byte{0, 0, 0, 202})} + testTree := NewDefaultTreeWithPrefix(nil, "prefix", []byte{1, 0, 0, 0}) + props := []proofs.Property{NewLeafProperty("prefix.sample_field", []byte{1, 0, 0, 0, 0, 0, 0, 200}), NewLeafProperty("prefix.sample_field2", []byte{1, 0, 0, 0, 0, 0, 0, 202})} compactProps := [][]byte{props[0].Compact, props[1].Compact} err := testTree.AddLeaf(proofs.LeafNode{Hash: utils.RandomSlice(32), Hashed: true, Property: props[0]}) assert.NoError(t, err) @@ -274,7 +332,7 @@ func TestCoreDocument_GenerateProofs(t *testing.T) { proofLength int }{ { - "sample_field", + "prefix.sample_field", false, 3, }, @@ -284,7 +342,7 @@ func TestCoreDocument_GenerateProofs(t *testing.T) { 6, }, { - "sample_field2", + "prefix.sample_field2", false, 3, }, @@ -334,7 +392,7 @@ func TestCoreDocument_getCollaborators(t *testing.T) { id1 := testingidentity.GenerateRandomDID() id2 := testingidentity.GenerateRandomDID() ids := []string{id1.String()} - cd, err := NewCoreDocumentWithCollaborators(ids) + cd, err := NewCoreDocumentWithCollaborators(ids, nil) assert.NoError(t, err) cs, err := cd.getCollaborators(coredocumentpb.Action_ACTION_READ_SIGN) assert.NoError(t, err) @@ -344,10 +402,10 @@ func TestCoreDocument_getCollaborators(t *testing.T) { cs, err = cd.getCollaborators(coredocumentpb.Action_ACTION_READ) assert.NoError(t, err) assert.Len(t, cs, 0) - role := newRole() role.Collaborators = append(role.Collaborators, id2[:]) - cd.addNewRule(role, coredocumentpb.Action_ACTION_READ) + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewReadRule(role.RoleKey, coredocumentpb.Action_ACTION_READ) cs, err = cd.getCollaborators(coredocumentpb.Action_ACTION_READ) assert.NoError(t, err) @@ -364,8 +422,9 @@ func TestCoreDocument_getCollaborators(t *testing.T) { func TestCoreDocument_GetCollaborators(t *testing.T) { id1 := testingidentity.GenerateRandomDID() id2 := testingidentity.GenerateRandomDID() + id3 := testingidentity.GenerateRandomDID() ids := []string{id1.String()} - cd, err := NewCoreDocumentWithCollaborators(ids) + cd, err := NewCoreDocumentWithCollaborators(ids, nil) assert.NoError(t, err) cs, err := cd.GetCollaborators() assert.NoError(t, err) @@ -378,7 +437,8 @@ func TestCoreDocument_GetCollaborators(t *testing.T) { role := newRole() role.Collaborators = append(role.Collaborators, id2[:]) - cd.addNewRule(role, coredocumentpb.Action_ACTION_READ) + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewReadRule(role.RoleKey, coredocumentpb.Action_ACTION_READ) cs, err = cd.GetCollaborators() assert.NoError(t, err) @@ -390,13 +450,18 @@ func TestCoreDocument_GetCollaborators(t *testing.T) { assert.NoError(t, err) assert.Len(t, cs, 1) assert.Contains(t, cs, id1) + + role2 := newRole() + role2.Collaborators = append(role.Collaborators, id3[:]) + cd.Document.Roles = append(cd.Document.Roles, role2) + cd.addNewTransitionRule(role2.RoleKey, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX, nil, coredocumentpb.TransitionAction_TRANSITION_ACTION_EDIT) } func TestCoreDocument_GetSignCollaborators(t *testing.T) { id1 := testingidentity.GenerateRandomDID() id2 := testingidentity.GenerateRandomDID() ids := []string{id1.String()} - cd, err := NewCoreDocumentWithCollaborators(ids) + cd, err := NewCoreDocumentWithCollaborators(ids, nil) assert.NoError(t, err) cs, err := cd.GetSignerCollaborators() assert.NoError(t, err) @@ -409,7 +474,8 @@ func TestCoreDocument_GetSignCollaborators(t *testing.T) { role := newRole() role.Collaborators = append(role.Collaborators, id2[:]) - cd.addNewRule(role, coredocumentpb.Action_ACTION_READ) + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewReadRule(role.RoleKey, coredocumentpb.Action_ACTION_READ) cs, err = cd.GetSignerCollaborators() assert.NoError(t, err) diff --git a/documents/documents_test/service_test.go b/documents/documents_test/service_test.go index 5075e8e73..507b9f70c 100644 --- a/documents/documents_test/service_test.go +++ b/documents/documents_test/service_test.go @@ -6,17 +6,20 @@ import ( "context" "os" "testing" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/testlogging" "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/config/configstore" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/storage/leveldb" "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/testingutils/config" @@ -29,11 +32,8 @@ import ( var testRepoGlobal documents.Repository var ( - cid = testingidentity.GenerateRandomDID() - centIDBytes = cid[:] - tenantID = cid[:] - key1Pub = [...]byte{230, 49, 10, 12, 200, 149, 43, 184, 145, 87, 163, 252, 114, 31, 91, 163, 24, 237, 36, 51, 165, 8, 34, 104, 97, 49, 114, 85, 255, 15, 195, 199} - key1 = []byte{102, 109, 71, 239, 130, 229, 128, 189, 37, 96, 223, 5, 189, 91, 210, 47, 89, 4, 165, 6, 188, 53, 49, 250, 109, 151, 234, 139, 57, 205, 231, 253, 230, 49, 10, 12, 200, 149, 43, 184, 145, 87, 163, 252, 114, 31, 91, 163, 24, 237, 36, 51, 165, 8, 34, 104, 97, 49, 114, 85, 255, 15, 195, 199} + did = testingidentity.GenerateRandomDID() + accountID = did[:] ) var ctx = map[string]interface{}{} @@ -43,30 +43,106 @@ func TestMain(m *testing.M) { ethClient := &testingcommons.MockEthClient{} ethClient.On("GetEthClient").Return(nil) ctx[ethereum.BootstrappedEthereumClient] = ethClient - ibootstappers := []bootstrap.TestBootstrapper{ + ibootstrappers := []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, ctx) + bootstrap.RunTestBootstrappers(ibootstrappers, ctx) cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) - cfg.Set("identityId", cid.String()) + cfg.Set("identityId", did.String()) result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) + bootstrap.RunTestTeardown(ibootstrappers) os.Exit(result) } -func TestService_ReceiveAnchoredDocumentFailed(t *testing.T) { - poSrv := documents.DefaultService(nil, nil, documents.NewServiceRegistry(), nil) +func TestService_ReceiveAnchoredDocument(t *testing.T) { + srv := documents.DefaultService(nil, nil, documents.NewServiceRegistry(), nil) // self failed - err := poSrv.ReceiveAnchoredDocument(context.Background(), nil, nil) + err := srv.ReceiveAnchoredDocument(context.Background(), nil, did) assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentConfigAccountID, err)) + + // nil model + ctxh := testingconfig.CreateAccountContext(t, cfg) + acc, err := contextutil.Account(ctxh) + assert.NoError(t, err) + err = srv.ReceiveAnchoredDocument(ctxh, nil, did) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) + + // first version of the document but not saved + id2 := testingidentity.GenerateRandomDID() + doc, cd := createCDWithEmbeddedInvoice(t, ctxh, []identity.DID{id2}, true) + idSrv := new(testingcommons.MockIdentityService) + idSrv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + ar := new(mockAnchorRepo) + dr, err := anchors.ToDocumentRoot(cd.DocumentRoot) + assert.NoError(t, err) + ar.On("GetAnchorData", mock.Anything).Return(dr, time.Now(), nil) + srv = documents.DefaultService(testRepo(), ar, documents.NewServiceRegistry(), idSrv) + err = srv.ReceiveAnchoredDocument(ctxh, doc, did) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentPersistence, err)) + ar.AssertExpectations(t) + idSrv.AssertExpectations(t) + + // new document with saved + doc, cd = createCDWithEmbeddedInvoice(t, ctxh, []identity.DID{id2}, false) + idSrv = new(testingcommons.MockIdentityService) + idSrv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + ar = new(mockAnchorRepo) + dr, err = anchors.ToDocumentRoot(cd.DocumentRoot) + assert.NoError(t, err) + ar.On("GetAnchorData", mock.Anything).Return(dr, time.Now(), nil) + srv = documents.DefaultService(testRepo(), ar, documents.NewServiceRegistry(), idSrv) + err = srv.ReceiveAnchoredDocument(ctxh, doc, did) + assert.NoError(t, err) + ar.AssertExpectations(t) + idSrv.AssertExpectations(t) + + // prepare a new version + err = doc.AddNFT(true, testingidentity.GenerateRandomDID().ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + err = doc.AddUpdateLog(did) + assert.NoError(t, err) + _, err = doc.CalculateDataRoot() + assert.NoError(t, err) + sr, err := doc.CalculateSigningRoot() + assert.NoError(t, err) + sig, err := acc.SignMsg(sr) + assert.NoError(t, err) + + doc.AppendSignatures(sig) + ndr, err := doc.CalculateDocumentRoot() + assert.NoError(t, err) + err = testRepo().Create(did[:], doc.CurrentVersion(), doc) + assert.NoError(t, err) + + // invalid transition for id3 + id3 := testingidentity.GenerateRandomDID() + err = srv.ReceiveAnchoredDocument(ctxh, doc, id3) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalid, err)) + assert.Contains(t, err.Error(), "invalid document state transition") + + // valid transition for id2 + idSrv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + ar = new(mockAnchorRepo) + dr, err = anchors.ToDocumentRoot(ndr) + assert.NoError(t, err) + ar.On("GetAnchorData", mock.Anything).Return(dr, time.Now(), nil) + srv = documents.DefaultService(testRepo(), ar, documents.NewServiceRegistry(), idSrv) + err = srv.ReceiveAnchoredDocument(ctxh, doc, id2) + assert.NoError(t, err) + ar.AssertExpectations(t) + idSrv.AssertExpectations(t) } func getServiceWithMockedLayers() (documents.Service, testingcommons.MockIdentityService) { repo := testRepo() idService := testingcommons.MockIdentityService{} - idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() mockAnchor = &mockAnchorRepo{} return documents.DefaultService(repo, mockAnchor, documents.NewServiceRegistry(), &idService), idService } @@ -78,10 +154,11 @@ type mockAnchorRepo struct { var mockAnchor *mockAnchorRepo -func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { +func (r *mockAnchorRepo) GetAnchorData(anchorID anchors.AnchorID) (docRoot anchors.DocumentRoot, anchoredTime time.Time, err error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocumentRoot) - return docRoot, args.Error(1) + docRoot, _ = args.Get(0).(anchors.DocumentRoot) + anchoredTime, _ = args.Get(1).(time.Time) + return docRoot, anchoredTime, args.Error(2) } // Functions returns service mocks @@ -91,14 +168,14 @@ func mockSignatureCheck(t *testing.T, i *invoice.Invoice, idService testingcommo assert.NoError(t, err) docRoot, err := anchors.ToDocumentRoot(dr) assert.NoError(t, err) - mockAnchor.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() + mockAnchor.On("GetAnchorData", anchorID).Return(docRoot, time.Now(), nil).Once() return idService } func TestService_CreateProofs(t *testing.T) { service, idService := getServiceWithMockedLayers() ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, false) + i, _ := createCDWithEmbeddedInvoice(t, ctxh, nil, false) idService = mockSignatureCheck(t, i.(*invoice.Invoice), idService) proof, err := service.CreateProofs(ctxh, i.ID(), []string{"invoice.invoice_number"}) assert.Nil(t, err) @@ -110,11 +187,11 @@ func TestService_CreateProofs(t *testing.T) { func TestService_CreateProofsValidationFails(t *testing.T) { service, idService := getServiceWithMockedLayers() ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, false) + i, _ := createCDWithEmbeddedInvoice(t, ctxh, nil, false) idService = mockSignatureCheck(t, i.(*invoice.Invoice), idService) i.(*invoice.Invoice).Document.DataRoot = nil i.(*invoice.Invoice).Document.SigningRoot = nil - assert.Nil(t, testRepo().Update(tenantID, i.CurrentVersion(), i)) + assert.Nil(t, testRepo().Update(accountID, i.CurrentVersion(), i)) _, err := service.CreateProofs(ctxh, i.ID(), []string{"invoice.invoice_number"}) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get signing root") @@ -123,7 +200,7 @@ func TestService_CreateProofsValidationFails(t *testing.T) { func TestService_CreateProofsInvalidField(t *testing.T) { service, idService := getServiceWithMockedLayers() ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, false) + i, _ := createCDWithEmbeddedInvoice(t, ctxh, nil, false) idService = mockSignatureCheck(t, i.(*invoice.Invoice), idService) _, err := service.CreateProofs(ctxh, i.CurrentVersion(), []string{"invalid_field"}) assert.Error(t, err) @@ -141,7 +218,7 @@ func TestService_CreateProofsDocumentDoesntExist(t *testing.T) { func TestService_CreateProofsForVersion(t *testing.T) { service, idService := getServiceWithMockedLayers() ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, false) + i, _ := createCDWithEmbeddedInvoice(t, ctxh, nil, false) idService = mockSignatureCheck(t, i.(*invoice.Invoice), idService) proof, err := service.CreateProofsForVersion(ctxh, i.ID(), i.CurrentVersion(), []string{"invoice.invoice_number"}) assert.Nil(t, err) @@ -151,22 +228,66 @@ func TestService_CreateProofsForVersion(t *testing.T) { assert.Equal(t, proof.FieldProofs[0].GetCompactName(), []byte{0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}) } -func TestService_RequestDocumentSignature_SigningRootNil(t *testing.T) { - service, idService := getServiceWithMockedLayers() - ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, true) - idService = mockSignatureCheck(t, i.(*invoice.Invoice), idService) - i.(*invoice.Invoice).Document.DataRoot = nil - i.(*invoice.Invoice).Document.SigningRoot = nil - signature, err := service.RequestDocumentSignature(ctxh, i) - assert.NotNil(t, err) +func TestService_RequestDocumentSignature(t *testing.T) { + srv, _ := getServiceWithMockedLayers() + + // self failed + _, err := srv.RequestDocumentSignature(context.Background(), nil, did) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentConfigAccountID, err)) + + // nil model + tc, err := configstore.NewAccount("main", cfg) + assert.NoError(t, err) + acc := tc.(*configstore.Account) + acc.IdentityID = did[:] + ctxh, err := contextutil.New(context.Background(), acc) + assert.NoError(t, err) + _, err = srv.RequestDocumentSignature(ctxh, nil, did) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) + + // add doc to repo + id := testingidentity.GenerateRandomDID() + doc, cd := createCDWithEmbeddedInvoice(t, ctxh, []identity.DID{id}, false) + idSrv := new(testingcommons.MockIdentityService) + idSrv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + ar := new(mockAnchorRepo) + dr, err := anchors.ToDocumentRoot(cd.DocumentRoot) + assert.NoError(t, err) + ar.On("GetDocumentRootOf", mock.Anything).Return(dr, nil) + srv = documents.DefaultService(testRepo(), ar, documents.NewServiceRegistry(), idSrv) + + // prepare a new version + err = doc.AddNFT(true, testingidentity.GenerateRandomDID().ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + err = doc.AddUpdateLog(did) + _, err = doc.CalculateDataRoot() + assert.NoError(t, err) + sr, err := doc.CalculateSigningRoot() + assert.NoError(t, err) + sig, err := acc.SignMsg(sr) + assert.NoError(t, err) + + doc.AppendSignatures(sig) + _, err = doc.CalculateDocumentRoot() + assert.NoError(t, err) + + // invalid transition + id2 := testingidentity.GenerateRandomDID() + _, err = srv.RequestDocumentSignature(ctxh, doc, id2) + assert.Error(t, err) assert.True(t, errors.IsOfType(documents.ErrDocumentInvalid, err)) - assert.Nil(t, signature) + assert.Contains(t, err.Error(), "invalid document state transition") + + // valid transition + _, err = srv.RequestDocumentSignature(ctxh, doc, id) + assert.NoError(t, err) } func TestService_CreateProofsForVersionDocumentDoesntExist(t *testing.T) { ctxh := testingconfig.CreateAccountContext(t, cfg) - i, _ := createCDWithEmbeddedInvoice(t, ctxh, false) + i, _ := createCDWithEmbeddedInvoice(t, ctxh, nil, false) s, _ := getServiceWithMockedLayers() _, err := s.CreateProofsForVersion(ctxh, i.ID(), utils.RandomSlice(32), []string{"invoice.invoice_number"}) assert.Error(t, err) @@ -203,7 +324,7 @@ func TestService_GetCurrentVersion_successful(t *testing.T) { CoreDocument: documents.NewCoreDocumentFromProtobuf(cd), } - err := testRepo().Create(tenantID, version, inv) + err := testRepo().Create(accountID, version, inv) currentVersion = version version = next assert.Nil(t, err) @@ -232,7 +353,7 @@ func TestService_GetVersion_successful(t *testing.T) { } ctxh := testingconfig.CreateAccountContext(t, cfg) - err := testRepo().Create(tenantID, currentVersion, inv) + err := testRepo().Create(accountID, currentVersion, inv) assert.Nil(t, err) mod, err := service.GetVersion(ctxh, documentIdentifier, currentVersion) @@ -261,7 +382,7 @@ func TestService_GetCurrentVersion_error(t *testing.T) { CoreDocument: documents.NewCoreDocumentFromProtobuf(cd), } - err = testRepo().Create(tenantID, documentIdentifier, inv) + err = testRepo().Create(accountID, documentIdentifier, inv) assert.Nil(t, err) _, err = service.GetCurrentVersion(ctxh, documentIdentifier) @@ -288,7 +409,7 @@ func TestService_GetVersion_error(t *testing.T) { GrossAmount: 60, CoreDocument: documents.NewCoreDocumentFromProtobuf(cd), } - err = testRepo().Create(tenantID, currentVersion, inv) + err = testRepo().Create(accountID, currentVersion, inv) assert.Nil(t, err) //random version @@ -330,7 +451,7 @@ func TestService_Exists(t *testing.T) { CoreDocument: documents.NewCoreDocumentFromProtobuf(cd), } - err = testRepo().Create(tenantID, documentIdentifier, inv) + err = testRepo().Create(accountID, documentIdentifier, inv) exists := service.Exists(ctxh, documentIdentifier) assert.True(t, exists, "document should exist") @@ -340,9 +461,20 @@ func TestService_Exists(t *testing.T) { } -func createCDWithEmbeddedInvoice(t *testing.T, ctx context.Context, skipSave bool) (documents.Model, coredocumentpb.CoreDocument) { +func createCDWithEmbeddedInvoice(t *testing.T, ctx context.Context, collaborators []identity.DID, skipSave bool) (documents.Model, coredocumentpb.CoreDocument) { i := new(invoice.Invoice) - err := i.InitInvoiceInput(testingdocuments.CreateInvoicePayload(), cid.String()) + data := testingdocuments.CreateInvoicePayload() + if len(collaborators) > 0 { + var cs []string + for _, c := range collaborators { + cs = append(cs, c.String()) + } + data.Collaborators = cs + } + + err := i.InitInvoiceInput(data, did.String()) + assert.NoError(t, err) + err = i.AddUpdateLog(did) assert.NoError(t, err) _, err = i.CalculateDataRoot() assert.NoError(t, err) @@ -362,7 +494,7 @@ func createCDWithEmbeddedInvoice(t *testing.T, ctx context.Context, skipSave boo assert.NoError(t, err) if !skipSave { - err = testRepo().Create(tenantID, i.CurrentVersion(), i) + err = testRepo().Create(accountID, i.CurrentVersion(), i) assert.NoError(t, err) } return i, cd diff --git a/documents/error.go b/documents/error.go index ad51e7d95..35796a801 100644 --- a/documents/error.go +++ b/documents/error.go @@ -23,6 +23,9 @@ const ( // ErrDocumentNil must be used when the provided document through a function is nil ErrDocumentNil = errors.Error("no(nil) document provided") + // ErrDocumentNotification must be used when a notification about a document could not be delivered + ErrDocumentNotification = errors.Error("could not notify of the document") + // ErrDocumentInvalid must only be used when the reason for invalidity is impossible to determine or the invalidity is caused by validation errors ErrDocumentInvalid = errors.Error("document is invalid") @@ -78,6 +81,9 @@ const ( // ErrInvalidIDLength must be used when the identifier bytelength is not 32 ErrInvalidIDLength = errors.Error("invalid identifier length") + + // ErrEmptyCollabs must be used when a given collaborators array is empty + ErrEmptyCollabs = errors.Error("empty collaborators") ) // Error wraps an error with specific key diff --git a/documents/invoice/model.go b/documents/invoice/model.go index a094dfe77..4b781e18a 100644 --- a/documents/invoice/model.go +++ b/documents/invoice/model.go @@ -3,6 +3,7 @@ package invoice import ( "encoding/json" "reflect" + "time" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" @@ -160,7 +161,7 @@ func (i *Invoice) InitInvoiceInput(payload *clientinvoicepb.InvoiceCreatePayload } collaborators := append([]string{self}, payload.Collaborators...) - cd, err := documents.NewCoreDocumentWithCollaborators(collaborators) + cd, err := documents.NewCoreDocumentWithCollaborators(collaborators, compactPrefix()) if err != nil { return errors.New("failed to init core document: %v", err) } @@ -393,7 +394,7 @@ func (i *Invoice) PrepareNewVersion(old documents.Model, data *clientinvoicepb.I } oldCD := old.(*Invoice).CoreDocument - i.CoreDocument, err = oldCD.PrepareNewVersion(collaborators, true) + i.CoreDocument, err = oldCD.PrepareNewVersion(collaborators, true, compactPrefix()) if err != nil { return err } @@ -427,3 +428,47 @@ func (i *Invoice) CreateNFTProofs( i.DocumentType(), account, registry, tokenID, nftUniqueProof, readAccessProof) } + +// CollaboratorCanUpdate checks if the collaborator can update the document. +func (i *Invoice) CollaboratorCanUpdate(updated documents.Model, collaborator identity.DID) error { + newInv, ok := updated.(*Invoice) + if !ok { + return errors.NewTypedError(documents.ErrDocumentInvalidType, errors.New("expecting an invoice but got %T", updated)) + } + + // check the core document changes + err := i.CoreDocument.CollaboratorCanUpdate(newInv.CoreDocument, collaborator, i.DocumentType()) + if err != nil { + return err + } + + // check invoice specific changes + oldTree, err := i.getDocumentDataTree() + if err != nil { + return err + } + + newTree, err := newInv.getDocumentDataTree() + if err != nil { + return err + } + + rules := i.CoreDocument.TransitionRulesFor(collaborator) + cf := documents.GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + return documents.ValidateTransitions(rules, cf) +} + +// AddUpdateLog adds a log to the model to persist an update related meta data such as author +func (i *Invoice) AddUpdateLog(account identity.DID) (err error) { + return i.CoreDocument.AddUpdateLog(account) +} + +// Author is the author of the document version represented by the model +func (i *Invoice) Author() identity.DID { + return i.CoreDocument.Author() +} + +// Timestamp is the time of update in UTC of the document version represented by the model +func (i *Invoice) Timestamp() (time.Time, error) { + return i.CoreDocument.Timestamp() +} diff --git a/documents/invoice/model_test.go b/documents/invoice/model_test.go index 96a709c18..fd28a6ee1 100644 --- a/documents/invoice/model_test.go +++ b/documents/invoice/model_test.go @@ -7,6 +7,9 @@ import ( "fmt" "os" "testing" + "time" + + "github.com/golang/protobuf/ptypes/timestamp" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" @@ -17,6 +20,7 @@ import ( "github.com/centrifuge/go-centrifuge/config/configstore" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/identity/ideth" @@ -238,8 +242,7 @@ func TestInvoiceModel_calculateDataRoot(t *testing.T) { } func TestInvoice_CreateProofs(t *testing.T) { - i, err := createInvoice(t) - assert.Nil(t, err) + i := createInvoice(t) rk := i.Document.Roles[0].RoleKey pf := fmt.Sprintf(documents.CDTreePrefix+".roles[%s].collaborators[0]", hexutil.Encode(rk)) proof, err := i.CreateProofs([]string{"invoice.invoice_number", pf, documents.CDTreePrefix + ".document_type"}) @@ -268,16 +271,71 @@ func TestInvoice_CreateProofs(t *testing.T) { assert.True(t, valid) } -func TestInvoiceModel_createProofsFieldDoesNotExist(t *testing.T) { - i, err := createInvoice(t) +func TestInvoice_CreateNFTProofs(t *testing.T) { + tc, err := configstore.NewAccount("main", cfg) + acc := tc.(*configstore.Account) + acc.IdentityID = defaultDID[:] + assert.NoError(t, err) + i := new(Invoice) + invPayload := testingdocuments.CreateInvoicePayload() + invPayload.Data.DueDate = ×tamp.Timestamp{Seconds: time.Now().Unix()} + invPayload.Data.InvoiceStatus = "unpaid" + invPayload.Collaborators = []string{defaultDID.String()} + + err = i.InitInvoiceInput(invPayload, defaultDID.String()) + assert.NoError(t, err) + sig, err := acc.SignMsg([]byte{0, 1, 2, 3}) + assert.NoError(t, err) + i.AppendSignatures(sig) + _, err = i.CalculateDataRoot() + assert.NoError(t, err) + _, err = i.CalculateSigningRoot() + assert.NoError(t, err) + _, err = i.CalculateDocumentRoot() + assert.NoError(t, err) + + keys, err := tc.GetKeys() + assert.NoError(t, err) + signerId := hexutil.Encode(append(defaultDID[:], keys[identity.KeyPurposeSigning.Name].PublicKey...)) + signingRoot := fmt.Sprintf("%s.%s", documents.DRTreePrefix, documents.SigningRootField) + signatureSender := fmt.Sprintf("%s.signatures[%s].signature", documents.SignaturesTreePrefix, signerId) + proofFields := []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "invoice.sender", "invoice.invoice_status", signingRoot, signatureSender, documents.CDTreePrefix + ".next_version"} + proof, err := i.CreateProofs(proofFields) assert.Nil(t, err) - _, err = i.CreateProofs([]string{"nonexisting"}) + assert.NotNil(t, proof) + tree, err := i.CoreDocument.DocumentRootTree() + assert.NoError(t, err) + assert.Len(t, proofFields, 8) + + // Validate invoice_gross_amount + valid, err := tree.ValidateProof(proof[0]) + assert.Nil(t, err) + assert.True(t, valid) + + // Validate signing_root + valid, err = tree.ValidateProof(proof[5]) + assert.Nil(t, err) + assert.True(t, valid) + + // Validate signature + valid, err = tree.ValidateProof(proof[6]) + assert.Nil(t, err) + assert.True(t, valid) + + // Validate next_version + valid, err = tree.ValidateProof(proof[7]) + assert.Nil(t, err) + assert.True(t, valid) +} + +func TestInvoiceModel_createProofsFieldDoesNotExist(t *testing.T) { + i := createInvoice(t) + _, err := i.CreateProofs([]string{"nonexisting"}) assert.NotNil(t, err) } func TestInvoiceModel_GetDocumentID(t *testing.T) { - i, err := createInvoice(t) - assert.Nil(t, err) + i := createInvoice(t) assert.Equal(t, i.CoreDocument.ID(), i.ID()) } @@ -290,7 +348,7 @@ func TestInvoiceModel_getDocumentDataTree(t *testing.T) { assert.Equal(t, "invoice.invoice_number", leaf.Property.ReadableName()) } -func createInvoice(t *testing.T) (*Invoice, error) { +func createInvoice(t *testing.T) *Invoice { i := new(Invoice) err := i.InitInvoiceInput(testingdocuments.CreateInvoicePayload(), defaultDID.String()) assert.NoError(t, err) @@ -300,5 +358,61 @@ func createInvoice(t *testing.T) (*Invoice, error) { assert.NoError(t, err) _, err = i.CalculateDocumentRoot() assert.NoError(t, err) - return i, nil + return i +} + +func TestInvoice_CollaboratorCanUpdate(t *testing.T) { + inv := createInvoice(t) + id1 := defaultDID + id2 := testingidentity.GenerateRandomDID() + id3 := testingidentity.GenerateRandomDID() + + // wrong type + err := inv.CollaboratorCanUpdate(new(mockModel), id1) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalidType, err)) + assert.NoError(t, testRepo().Create(id1[:], inv.CurrentVersion(), inv)) + + // update the document + model, err := testRepo().Get(id1[:], inv.CurrentVersion()) + assert.NoError(t, err) + oldInv := model.(*Invoice) + data := oldInv.getClientData() + data.GrossAmount = 50 + err = inv.PrepareNewVersion(inv, data, []string{id3.String()}) + assert.NoError(t, err) + + // id1 should have permission + assert.NoError(t, oldInv.CollaboratorCanUpdate(inv, id1)) + + // id2 should fail since it doesn't have the permission to update + assert.Error(t, oldInv.CollaboratorCanUpdate(inv, id2)) + + // update the id3 rules to update only gross amount + inv.CoreDocument.Document.TransitionRules[3].MatchType = coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_EXACT + inv.CoreDocument.Document.TransitionRules[3].Field = append(compactPrefix(), 0, 0, 0, 14) + inv.CoreDocument.Document.DocumentRoot = utils.RandomSlice(32) + assert.NoError(t, testRepo().Create(id1[:], inv.CurrentVersion(), inv)) + + // fetch the document + model, err = testRepo().Get(id1[:], inv.CurrentVersion()) + assert.NoError(t, err) + oldInv = model.(*Invoice) + data = oldInv.getClientData() + data.GrossAmount = 55 + data.Currency = "INR" + err = inv.PrepareNewVersion(inv, data, nil) + assert.NoError(t, err) + + // id1 should have permission + assert.NoError(t, oldInv.CollaboratorCanUpdate(inv, id1)) + + // id2 should fail since it doesn't have the permission to update + assert.Error(t, oldInv.CollaboratorCanUpdate(inv, id2)) + + // id3 should fail with just one error since changing Currency is not allowed + err = oldInv.CollaboratorCanUpdate(inv, id3) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + assert.Contains(t, err.Error(), "invoice.currency") } diff --git a/documents/invoice/service_test.go b/documents/invoice/service_test.go index 5708e534d..4a2c8f2d8 100644 --- a/documents/invoice/service_test.go +++ b/documents/invoice/service_test.go @@ -4,6 +4,7 @@ package invoice import ( "testing" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/anchors" @@ -36,10 +37,11 @@ type mockAnchorRepo struct { anchors.AnchorRepository } -func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { +func (r *mockAnchorRepo) GetAnchorData(anchorID anchors.AnchorID) (docRoot anchors.DocumentRoot, anchoredTime time.Time, err error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocumentRoot) - return docRoot, args.Error(1) + docRoot, _ = args.Get(0).(anchors.DocumentRoot) + anchoredTime, _ = args.Get(1).(time.Time) + return docRoot, anchoredTime, args.Error(2) } func getServiceWithMockedLayers() (testingcommons.MockIdentityService, Service) { diff --git a/documents/model.go b/documents/model.go index dab671d1d..e2836d77c 100644 --- a/documents/model.go +++ b/documents/model.go @@ -2,6 +2,7 @@ package documents import ( "context" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/identity" @@ -50,8 +51,11 @@ type Model interface { // CalculateDocumentRoot returns the Document root of the model. CalculateDocumentRoot() ([]byte, error) - // GetSigningRootProof get the proof for signing root of the model. - GetSigningRootProof() (hashes [][]byte, err error) + // GetSigningRootHash get the hash for signing root of the model. + GetSigningRootHash() (hash []byte, err error) + + // GetSignaturesRootHash get hash for the signatures root of the model + GetSignaturesRootHash() (hash []byte, err error) // PreviousDocumentRoot returns the Document root of the previous version. PreviousDocumentRoot() []byte @@ -95,6 +99,18 @@ type Model interface { // ATGranteeCanRead returns error if the access token grantee cannot read the document. ATGranteeCanRead(ctx context.Context, idSrv identity.ServiceDID, tokenID, docID []byte, grantee identity.DID) (err error) + + // AddUpdateLog adds a log to the model to persist an update related meta data such as author + AddUpdateLog(account identity.DID) error + + // Author is the author of the document version represented by the model + Author() identity.DID + + // Timestamp is the time of update in UTC of the document version represented by the model + Timestamp() (time.Time, error) + + // CollaboratorCanUpdate returns an error if indicated identity does not have the capacity to update the document. + CollaboratorCanUpdate(updated Model, collaborator identity.DID) error } // TokenRegistry defines NFT related functions. diff --git a/documents/processor.go b/documents/processor.go index 95b15d6a3..ef5b6edcc 100644 --- a/documents/processor.go +++ b/documents/processor.go @@ -75,6 +75,16 @@ func (dp defaultProcessor) PrepareForSignatureRequests(ctx context.Context, mode return err } + id, err := self.GetIdentityID() + if err != nil { + return err + } + + err = model.AddUpdateLog(identity.NewDIDFromBytes(id)) + if err != nil { + return err + } + // calculate the signing root sr, err := model.CalculateSigningRoot() if err != nil { @@ -174,12 +184,12 @@ func (dp defaultProcessor) AnchorDocument(ctx context.Context, model Model) erro return errors.New("failed to get anchor ID: %v", err) } - signingRootProof, err := model.GetSigningRootProof() + signingRootProof, err := model.GetSignaturesRootHash() if err != nil { return errors.New("failed to get signing root proof: %v", err) } - signingRootProofHashes, err := utils.ConvertProofForEthereum(signingRootProof) + signingRootProofHashes, err := utils.ConvertProofForEthereum([][]byte{signingRootProof}) if err != nil { return errors.New("failed to get signing root proof in ethereum format: %v", err) } diff --git a/documents/processor_test.go b/documents/processor_test.go index 9bfae99f9..7783c567f 100644 --- a/documents/processor_test.go +++ b/documents/processor_test.go @@ -5,6 +5,7 @@ package documents import ( "context" "testing" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" @@ -45,9 +46,9 @@ func (m *mockModel) CalculateDocumentRoot() ([]byte, error) { return dr, args.Error(1) } -func (m *mockModel) GetSigningRootProof() (hashes [][]byte, err error) { +func (m *mockModel) GetSignaturesRootHash() (hashes []byte, err error) { args := m.Called() - dr, _ := args.Get(0).([][]byte) + dr, _ := args.Get(0).([]byte) return dr, args.Error(1) } @@ -101,6 +102,23 @@ func (m *mockModel) Signatures() []coredocumentpb.Signature { return ss } +func (m *mockModel) AddUpdateLog(account identity.DID) error { + args := m.Called() + return args.Error(0) +} + +func (m *mockModel) Author() identity.DID { + args := m.Called() + id, _ := args.Get(0).(identity.DID) + return id +} + +func (m *mockModel) Timestamp() (time.Time, error) { + args := m.Called() + dr, _ := args.Get(0).(time.Time) + return dr, args.Error(1) +} + func (m *mockModel) GetCollaborators(filterIDs ...identity.DID) ([]identity.DID, error) { args := m.Called(filterIDs) cids, _ := args.Get(0).([]identity.DID) @@ -119,6 +137,11 @@ func (m *mockModel) PackCoreDocument() (coredocumentpb.CoreDocument, error) { return cd, args.Error(1) } +func (m *mockModel) CollaboratorCanUpdate(new Model, collaborator identity.DID) error { + args := m.Called(new, collaborator) + return args.Error(0) +} + func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { srv := &testingcommons.MockIdentityService{} dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) @@ -141,6 +164,7 @@ func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { // failed signing root model = new(mockModel) model.On("CalculateDataRoot").Return(utils.RandomSlice(32), nil).Once() + model.On("AddUpdateLog").Return(nil).Once() model.On("CalculateSigningRoot").Return(nil, errors.New("failed signing root")).Once() err = dp.PrepareForSignatureRequests(ctxh, model) model.AssertExpectations(t) @@ -152,6 +176,7 @@ func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { model = new(mockModel) model.On("CalculateDataRoot").Return(utils.RandomSlice(32), nil).Once() model.On("CalculateSigningRoot").Return(sr, nil).Once() + model.On("AddUpdateLog").Return(nil).Once() model.On("AppendSignatures", mock.Anything).Return().Once() err = dp.PrepareForSignatureRequests(ctxh, model) model.AssertExpectations(t) @@ -190,6 +215,8 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { self, err := contextutil.Account(ctxh) assert.NoError(t, err) + did, err := self.GetIdentityID() + assert.NoError(t, err) sr := utils.RandomSlice(32) sig, err := self.SignMsg(sr) assert.NoError(t, err) @@ -204,6 +231,8 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { model.AssertExpectations(t) assert.Error(t, err) + did1 := identity.NewDIDFromBytes(did) + // key validation failed model = new(mockModel) id := utils.RandomSlice(32) @@ -213,9 +242,12 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { model.On("NextVersion").Return(next) model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() + model.On("Author").Return(did1) + model.On("Timestamp").Return(time.Now(), nil) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) model.sigs = append(model.sigs, sig) c := new(p2pClient) - srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(errors.New("cannot validate key")).Once() + srv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("cannot validate key")).Once() err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) c.AssertExpectations(t) @@ -229,9 +261,12 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { model.On("NextVersion").Return(next) model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() + model.On("Author").Return(did1) + model.On("Timestamp").Return(time.Now(), nil) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) model.sigs = append(model.sigs, sig) c = new(p2pClient) - srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + srv.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) c.On("GetSignaturesForDocument", ctxh, model).Return(nil, errors.New("failed to get signatures")).Once() dp.p2pClient = c err = dp.RequestSignatures(ctxh, model) @@ -248,6 +283,9 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("AppendSignatures", []*coredocumentpb.Signature{sig}).Return().Once() + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(time.Now(), nil) model.sigs = append(model.sigs, sig) c = new(p2pClient) c.On("GetSignaturesForDocument", ctxh, model).Return([]*coredocumentpb.Signature{sig}, nil).Once() @@ -265,9 +303,12 @@ func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { ctxh := testingconfig.CreateAccountContext(t, cfg) self, err := contextutil.Account(ctxh) assert.NoError(t, err) + did, err := self.GetIdentityID() + assert.NoError(t, err) sr := utils.RandomSlice(32) sig, err := self.SignMsg(sr) assert.NoError(t, err) + did1 := identity.NewDIDFromBytes(did) // validation failed model := new(mockModel) @@ -278,9 +319,13 @@ func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { model.On("NextVersion").Return(next) model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + tm := time.Now() + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(errors.New("validation failed")).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(did), sig.PublicKey, sig.Signature, sr, tm).Return(errors.New("validation failed")).Once() dp.identityService = srv err = dp.PrepareForAnchoring(model) model.AssertExpectations(t) @@ -294,9 +339,12 @@ func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { model.On("NextVersion").Return(next) model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(did), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv err = dp.PrepareForAnchoring(model) model.AssertExpectations(t) @@ -315,10 +363,11 @@ func (m mockRepo) CommitAnchor(ctx context.Context, anchorID anchors.AnchorID, d return c, args.Error(1) } -func (m mockRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { +func (m mockRepo) GetAnchorData(anchorID anchors.AnchorID) (docRoot anchors.DocumentRoot, anchoredTime time.Time, err error) { args := m.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocumentRoot) - return docRoot, args.Error(1) + docRoot, _ = args.Get(0).(anchors.DocumentRoot) + anchoredTime, _ = args.Get(1).(time.Time) + return docRoot, anchoredTime, args.Error(2) } func TestDefaultProcessor_AnchorDocument(t *testing.T) { @@ -327,9 +376,12 @@ func TestDefaultProcessor_AnchorDocument(t *testing.T) { ctxh := testingconfig.CreateAccountContext(t, cfg) self, err := contextutil.Account(ctxh) assert.NoError(t, err) + did, err := self.GetIdentityID() + assert.NoError(t, err) sr := utils.RandomSlice(32) sig, err := self.SignMsg(sr) assert.NoError(t, err) + did1 := identity.NewDIDFromBytes(did) // validations failed id := utils.RandomSlice(32) @@ -341,9 +393,13 @@ func TestDefaultProcessor_AnchorDocument(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(nil, errors.New("error")) + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + tm := time.Now() + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(did), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) @@ -358,12 +414,15 @@ func TestDefaultProcessor_AnchorDocument(t *testing.T) { model.On("CurrentVersionPreimage").Return(id) model.On("NextVersion").Return(next) model.On("CalculateSigningRoot").Return(sr, nil) - model.On("GetSigningRootProof").Return([][32]byte{utils.RandomByte32()}, nil) + model.On("GetSignaturesRootHash").Return(utils.RandomByte32(), nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(utils.RandomSlice(32), nil) + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(did), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv repo := mockRepo{} ch := make(chan bool, 1) @@ -384,6 +443,9 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { ctxh := testingconfig.CreateAccountContext(t, cfg) self, err := contextutil.Account(ctxh) assert.NoError(t, err) + didb, err := self.GetIdentityID() + assert.NoError(t, err) + did1 := identity.NewDIDFromBytes(didb) sr := utils.RandomSlice(32) sig, err := self.SignMsg(sr) assert.NoError(t, err) @@ -400,12 +462,16 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(utils.RandomSlice(32), nil) + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + tm := time.Now() + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(didb), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv repo := mockRepo{} - repo.On("GetDocumentRootOf", aid).Return(nil, errors.New("error")) + repo.On("GetAnchorData", aid).Return(nil, time.Now(), errors.New("error")) dp.anchorRepository = repo err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) @@ -425,12 +491,13 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(dr[:], nil) model.On("GetSignerCollaborators", mock.Anything).Return(nil, errors.New("error")).Once() + model.On("Author").Return(did1) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() dp.identityService = srv repo = mockRepo{} - repo.On("GetDocumentRootOf", aid).Return(dr, nil).Once() + repo.On("GetAnchorData", aid).Return(dr, time.Now(), nil).Once() dp.anchorRepository = repo err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) @@ -446,14 +513,16 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(dr[:], nil) - model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{testingidentity.GenerateRandomDID()}, nil).Once() + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{testingidentity.GenerateRandomDID()}, nil) model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + model.On("Author").Return(did1) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(didb), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv repo = mockRepo{} - repo.On("GetDocumentRootOf", aid).Return(dr, nil).Once() + repo.On("GetAnchorData", aid).Return(dr, time.Now(), nil).Once() dp.anchorRepository = repo err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) @@ -471,14 +540,16 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(dr[:], nil) - model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did}, nil).Once() + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did}, nil) model.On("PackCoreDocument").Return(cd, nil).Once() + model.On("Author").Return(did1) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(didb), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv repo = mockRepo{} - repo.On("GetDocumentRootOf", aid).Return(dr, nil).Once() + repo.On("GetAnchorData", aid).Return(dr, time.Now(), nil).Once() client := new(p2pClient) client.On("SendAnchoredDocument", mock.Anything, did, mock.Anything).Return(nil, errors.New("error")).Once() dp.anchorRepository = repo @@ -498,14 +569,16 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model.On("CalculateSigningRoot").Return(sr, nil) model.On("Signatures").Return() model.On("CalculateDocumentRoot").Return(dr[:], nil) - model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did}, nil).Once() + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did}, nil) model.On("PackCoreDocument").Return(cd, nil).Once() + model.On("Author").Return(did1) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, sig) srv = &testingcommons.MockIdentityService{} - srv.On("ValidateSignature", sig, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(didb), sig.PublicKey, sig.Signature, sr, tm).Return(nil).Once() dp.identityService = srv repo = mockRepo{} - repo.On("GetDocumentRootOf", aid).Return(dr, nil).Once() + repo.On("GetAnchorData", aid).Return(dr, time.Now(), nil).Once() client = new(p2pClient) client.On("SendAnchoredDocument", mock.Anything, did, mock.Anything).Return(&p2ppb.AnchorDocumentResponse{Accepted: true}, nil).Once() dp.anchorRepository = repo diff --git a/documents/purchaseorder/model.go b/documents/purchaseorder/model.go index b472e2fc3..64ff0475e 100644 --- a/documents/purchaseorder/model.go +++ b/documents/purchaseorder/model.go @@ -3,6 +3,7 @@ package purchaseorder import ( "encoding/json" "reflect" + "time" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" @@ -146,7 +147,7 @@ func (p *PurchaseOrder) InitPurchaseOrderInput(payload *clientpurchaseorderpb.Pu } collaborators := append([]string{self}, payload.Collaborators...) - cd, err := documents.NewCoreDocumentWithCollaborators(collaborators) + cd, err := documents.NewCoreDocumentWithCollaborators(collaborators, compactPrefix()) if err != nil { return errors.New("failed to init core document: %v", err) } @@ -370,7 +371,7 @@ func (p *PurchaseOrder) PrepareNewVersion(old documents.Model, data *clientpurch } oldCD := old.(*PurchaseOrder).CoreDocument - p.CoreDocument, err = oldCD.PrepareNewVersion(collaborators, true) + p.CoreDocument, err = oldCD.PrepareNewVersion(collaborators, true, compactPrefix()) if err != nil { return err } @@ -405,3 +406,47 @@ func (p *PurchaseOrder) CreateNFTProofs( p.DocumentType(), account, registry, tokenID, nftUniqueProof, readAccessProof) } + +// CollaboratorCanUpdate checks if the account can update the document. +func (p *PurchaseOrder) CollaboratorCanUpdate(updated documents.Model, collaborator identity.DID) error { + newPo, ok := updated.(*PurchaseOrder) + if !ok { + return errors.NewTypedError(documents.ErrDocumentInvalidType, errors.New("expecting a purchase order but got %T", updated)) + } + + // check the core document changes + err := p.CoreDocument.CollaboratorCanUpdate(newPo.CoreDocument, collaborator, p.DocumentType()) + if err != nil { + return err + } + + // check purchase order specific changes + oldTree, err := p.getDocumentDataTree() + if err != nil { + return err + } + + newTree, err := newPo.getDocumentDataTree() + if err != nil { + return err + } + + rules := p.CoreDocument.TransitionRulesFor(collaborator) + cf := documents.GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + return documents.ValidateTransitions(rules, cf) +} + +// AddUpdateLog adds a log to the model to persist an update related meta data such as author +func (p *PurchaseOrder) AddUpdateLog(account identity.DID) (err error) { + return p.CoreDocument.AddUpdateLog(account) +} + +// Author is the author of the document version represented by the model +func (p *PurchaseOrder) Author() identity.DID { + return p.CoreDocument.Author() +} + +// Timestamp is the time of update in UTC of the document version represented by the model +func (p *PurchaseOrder) Timestamp() (time.Time, error) { + return p.CoreDocument.Timestamp() +} diff --git a/documents/purchaseorder/model_test.go b/documents/purchaseorder/model_test.go index 5feba9f2a..6f2910aae 100644 --- a/documents/purchaseorder/model_test.go +++ b/documents/purchaseorder/model_test.go @@ -17,6 +17,7 @@ import ( "github.com/centrifuge/go-centrifuge/config/configstore" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/identity/ideth" @@ -270,3 +271,59 @@ func createPurchaseOrder(t *testing.T) *PurchaseOrder { assert.NoError(t, err) return po } + +func TestPurchaseOrder_CollaboratorCanUpdate(t *testing.T) { + po := createPurchaseOrder(t) + id1 := defaultDID + id2 := testingidentity.GenerateRandomDID() + id3 := testingidentity.GenerateRandomDID() + + // wrong type + err := po.CollaboratorCanUpdate(new(mockModel), id1) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalidType, err)) + assert.NoError(t, testRepo().Create(id1[:], po.CurrentVersion(), po)) + + // update the document + model, err := testRepo().Get(id1[:], po.CurrentVersion()) + assert.NoError(t, err) + oldPO := model.(*PurchaseOrder) + data := oldPO.getClientData() + data.OrderAmount = 50 + err = po.PrepareNewVersion(po, data, []string{id3.String()}) + assert.NoError(t, err) + + // id1 should have permission + assert.NoError(t, oldPO.CollaboratorCanUpdate(po, id1)) + + // id2 should fail since it doesn't have the permission to update + assert.Error(t, oldPO.CollaboratorCanUpdate(po, id2)) + + // update the id3 rules to update only order amount + po.CoreDocument.Document.TransitionRules[3].MatchType = coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_EXACT + po.CoreDocument.Document.TransitionRules[3].Field = append(compactPrefix(), 0, 0, 0, 13) + po.CoreDocument.Document.DocumentRoot = utils.RandomSlice(32) + assert.NoError(t, testRepo().Create(id1[:], po.CurrentVersion(), po)) + + // fetch the document + model, err = testRepo().Get(id1[:], po.CurrentVersion()) + assert.NoError(t, err) + oldPO = model.(*PurchaseOrder) + data = oldPO.getClientData() + data.OrderAmount = 55 + data.Currency = "INR" + err = po.PrepareNewVersion(po, data, nil) + assert.NoError(t, err) + + // id1 should have permission + assert.NoError(t, oldPO.CollaboratorCanUpdate(po, id1)) + + // id2 should fail since it doesn't have the permission to update + assert.Error(t, oldPO.CollaboratorCanUpdate(po, id2)) + + // id3 should fail with just one error since changing Currency is not allowed + err = oldPO.CollaboratorCanUpdate(po, id3) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + assert.Contains(t, err.Error(), "po.currency") +} diff --git a/documents/purchaseorder/service_test.go b/documents/purchaseorder/service_test.go index 0556edddf..55ffecfc0 100644 --- a/documents/purchaseorder/service_test.go +++ b/documents/purchaseorder/service_test.go @@ -4,6 +4,7 @@ package purchaseorder import ( "testing" + "time" "github.com/centrifuge/go-centrifuge/testingutils/identity" @@ -36,10 +37,11 @@ type mockAnchorRepo struct { anchors.AnchorRepository } -func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { +func (r *mockAnchorRepo) GetAnchorData(anchorID anchors.AnchorID) (docRoot anchors.DocumentRoot, anchoredTime time.Time, err error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocumentRoot) - return docRoot, args.Error(1) + docRoot, _ = args.Get(0).(anchors.DocumentRoot) + anchoredTime, _ = args.Get(1).(time.Time) + return docRoot, anchoredTime, args.Error(2) } func getServiceWithMockedLayers() (*testingcommons.MockIdentityService, Service) { diff --git a/documents/read_acls.go b/documents/read_acls.go index 49e483eba..775d34881 100644 --- a/documents/read_acls.go +++ b/documents/read_acls.go @@ -33,6 +33,27 @@ func (cd *CoreDocument) initReadRules(collaborators []identity.DID) { cd.addCollaboratorsToReadSignRules(collaborators) } +// addCollaboratorsToReadSignRules adds the given collaborators to a new read rule with READ_SIGN capability. +// The operation is no-op if no collaborators are provided. +// The operation is not idempotent. So calling twice with same accounts will lead to read rules duplication. +func (cd *CoreDocument) addCollaboratorsToReadSignRules(collaborators []identity.DID) { + role := newRoleWithCollaborators(collaborators) + if role == nil { + return + } + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewReadRule(role.RoleKey, coredocumentpb.Action_ACTION_READ_SIGN) +} + +// addNewReadRule creates a new read rule as per the role and action. +func (cd *CoreDocument) addNewReadRule(roleKey []byte, action coredocumentpb.Action) { + rule := &coredocumentpb.ReadRule{ + Action: action, + Roles: [][]byte{roleKey}, + } + cd.Document.ReadRules = append(cd.Document.ReadRules, rule) +} + // findRole calls OnRole for every role that matches the actions passed in func findRole(cd coredocumentpb.CoreDocument, onRole func(rridx, ridx int, role *coredocumentpb.Role) bool, actions ...coredocumentpb.Action) bool { am := make(map[int32]struct{}) @@ -98,7 +119,7 @@ func (cd *CoreDocument) NFTOwnerCanRead(tokenRegistry TokenRegistry, registry co func (cd *CoreDocument) AccountCanRead(account identity.DID) bool { // loop though read rules, check all the rules return findRole(cd.Document, func(_, _ int, role *coredocumentpb.Role) bool { - _, found := isAccountInRole(role, account) + _, found := isDIDInRole(role, account) return found }, coredocumentpb.Action_ACTION_READ, coredocumentpb.Action_ACTION_READ_SIGN) } @@ -112,14 +133,15 @@ func (cd *CoreDocument) addNFTToReadRules(registry common.Address, tokenID []byt role := newRole() role.Nfts = append(role.Nfts, nft) - cd.addNewRule(role, coredocumentpb.Action_ACTION_READ) + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewReadRule(role.RoleKey, coredocumentpb.Action_ACTION_READ) return cd.setSalts() } // AddNFT returns a new CoreDocument model with nft added to the Core Document. If grantReadAccess is true, the nft is added // to the read rules. func (cd *CoreDocument) AddNFT(grantReadAccess bool, registry common.Address, tokenID []byte) (*CoreDocument, error) { - ncd, err := cd.PrepareNewVersion(nil, false) + ncd, err := cd.PrepareNewVersion(nil, false, nil) if err != nil { return nil, errors.New("failed to prepare new version: %v", err) } @@ -161,7 +183,7 @@ func (cd *CoreDocument) CreateNFTProofs( account identity.DID, registry common.Address, tokenID []byte, - nftUniqueProof, readAccessProof bool) (proofs []*proofspb.Proof, err error) { + nftUniqueProof, readAccessProof bool) (prfs []*proofspb.Proof, err error) { if len(cd.Document.DataRoot) != idSize { return nil, ErrDataRootInvalid @@ -186,9 +208,9 @@ func (cd *CoreDocument) CreateNFTProofs( pfKeys = append(pfKeys, pks...) } - signingRootProofHashes, err := cd.GetSigningRootProof() + signaturesTree, err := cd.getSignatureDataTree() if err != nil { - return nil, errors.New("failed to generate signing root proofs: %v", err) + return nil, errors.New("failed to get signatures tree: %v", err) } cdTree, err := cd.documentTree(docType) @@ -196,12 +218,8 @@ func (cd *CoreDocument) CreateNFTProofs( return nil, errors.New("failed to generate core Document tree: %v", err) } - proofs, missedProofs := generateProofs(cdTree, pfKeys, append([][]byte{cd.Document.DataRoot}, signingRootProofHashes...)) - if len(missedProofs) != 0 { - return nil, errors.New("failed to create proofs for fields %v", missedProofs) - } - - return proofs, nil + treeProofs := map[string]*TreeProof{CDTreePrefix: newTreeProof(cdTree, append([][]byte{cd.Document.DataRoot}, signaturesTree.RootHash()))} + return generateProofs(pfKeys, treeProofs) } // ConstructNFT appends registry and tokenID to byte slice @@ -292,7 +310,7 @@ func getRoleProofKey(roles []*coredocumentpb.Role, roleKey []byte, account ident return pk, err } - idx, found := isAccountInRole(role, account) + idx, found := isDIDInRole(role, account) if !found { return pk, ErrNFTRoleMissing } @@ -300,10 +318,10 @@ func getRoleProofKey(roles []*coredocumentpb.Role, roleKey []byte, account ident return fmt.Sprintf(CDTreePrefix+".roles[%s].collaborators[%d]", hexutil.Encode(role.RoleKey), idx), nil } -// isAccountInRole returns the index of the collaborator and true if account is in the given role as collaborators. -func isAccountInRole(role *coredocumentpb.Role, account identity.DID) (idx int, found bool) { +// isDIDInRole returns the index of the collaborator and true if did is in the given role as collaborators. +func isDIDInRole(role *coredocumentpb.Role, did identity.DID) (idx int, found bool) { for i, id := range role.Collaborators { - if bytes.Equal(id, account[:]) { + if bytes.Equal(id, did[:]) { return i, true } } @@ -370,7 +388,8 @@ func (cd *CoreDocument) ATGranteeCanRead(ctx context.Context, idService identity return ErrReqDocNotMatch } // validate that the public key of the granter is the public key that has been used to sign the access token - err = idService.ValidateKey(ctx, granterID, at.Key, &(identity.KeyPurposeSigning.Value)) + // TODO provide the time for validation here using the signature timestamp + err = idService.ValidateKey(ctx, granterID, at.Key, &(identity.KeyPurposeSigning.Value), nil) if err != nil { return err } @@ -379,7 +398,7 @@ func (cd *CoreDocument) ATGranteeCanRead(ctx context.Context, idService identity // AddAccessToken adds the AccessToken to the document func (cd *CoreDocument) AddAccessToken(ctx context.Context, payload documentpb.AccessTokenParams) (*CoreDocument, error) { - ncd, err := cd.PrepareNewVersion(nil, false) + ncd, err := cd.PrepareNewVersion(nil, false, nil) if err != nil { return nil, err } @@ -462,8 +481,3 @@ func assembleTokenMessage(tokenIdentifier []byte, granterID identity.DID, grante tm = append(tm, docID...) return tm, nil } - -// newRole returns a new role with random role key -func newRole() *coredocumentpb.Role { - return &coredocumentpb.Role{RoleKey: utils.RandomSlice(32)} -} diff --git a/documents/read_acls_test.go b/documents/read_acls_test.go index 88c50d867..bb063b102 100644 --- a/documents/read_acls_test.go +++ b/documents/read_acls_test.go @@ -47,7 +47,7 @@ func TestReadAccessValidator_AccountCanRead(t *testing.T) { assert.NoError(t, err) account := testingidentity.GenerateRandomDID() cd.Document.DocumentRoot = utils.RandomSlice(32) - ncd, err := cd.PrepareNewVersion([]string{account.String()}, false) + ncd, err := cd.PrepareNewVersion([]string{account.String()}, false, nil) assert.NoError(t, err) assert.NotNil(t, ncd.Document.ReadRules) assert.NotNil(t, ncd.Document.Roles) @@ -97,7 +97,7 @@ func TestCoreDocument_addNFTToReadRules(t *testing.T) { func TestCoreDocument_NFTOwnerCanRead(t *testing.T) { account := testingidentity.GenerateRandomDID() - cd, err := NewCoreDocumentWithCollaborators([]string{account.String()}) + cd, err := NewCoreDocumentWithCollaborators([]string{account.String()}, nil) assert.NoError(t, err) registry := common.HexToAddress("0xf72855759a39fb75fc7341139f5d7a3974d4da08") @@ -347,7 +347,7 @@ func TestCoreDocumentModel_ATOwnerCanRead(t *testing.T) { assert.NoError(t, err) granterID := identity.NewDIDFromBytes(id) assert.NoError(t, err) - cd, err := NewCoreDocumentWithCollaborators([]string{granterID.String()}) + cd, err := NewCoreDocumentWithCollaborators([]string{granterID.String()}, nil) assert.NoError(t, err) cd.Document.DocumentRoot = utils.RandomSlice(32) payload := documentpb.AccessTokenParams{ diff --git a/documents/service.go b/documents/service.go index 6d919d492..fda47b9ed 100644 --- a/documents/service.go +++ b/documents/service.go @@ -16,7 +16,6 @@ import ( "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs/proto" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/golang/protobuf/ptypes" logging "github.com/ipfs/go-log" ) @@ -50,10 +49,10 @@ type Service interface { CreateProofsForVersion(ctx context.Context, documentID, version []byte, fields []string) (*DocumentProof, error) // RequestDocumentSignature Validates and Signs document received over the p2p layer - RequestDocumentSignature(ctx context.Context, model Model) (*coredocumentpb.Signature, error) + RequestDocumentSignature(ctx context.Context, model Model, collaborator identity.DID) (*coredocumentpb.Signature, error) // ReceiveAnchoredDocument receives a new anchored document over the p2p layer, validates and updates the document in DB - ReceiveAnchoredDocument(ctx context.Context, model Model, senderID []byte) error + ReceiveAnchoredDocument(ctx context.Context, model Model, collaborator identity.DID) error // Create validates and persists Model and returns a Updated model Create(ctx context.Context, model Model) (Model, transactions.TxID, chan bool, error) @@ -150,13 +149,30 @@ func (s service) CreateProofsForVersion(ctx context.Context, documentID, version return s.createProofs(model, fields) } -func (s service) RequestDocumentSignature(ctx context.Context, model Model) (*coredocumentpb.Signature, error) { +func (s service) RequestDocumentSignature(ctx context.Context, model Model, collaborator identity.DID) (*coredocumentpb.Signature, error) { acc, err := contextutil.Account(ctx) if err != nil { return nil, ErrDocumentConfigAccountID } + idBytes, err := acc.GetIdentityID() + if err != nil { + return nil, err + } + did := identity.NewDIDFromBytes(idBytes) + if model == nil { + return nil, ErrDocumentNil + } + + var old Model + if !utils.IsEmptyByteSlice(model.PreviousVersion()) { + old, err = s.repo.Get(did[:], model.PreviousVersion()) + if err != nil { + // TODO: should pull old document from peer + log.Infof("failed to fetch previous document: %v", err) + } + } - if err := SignatureRequestValidator(s.idService).Validate(nil, model); err != nil { + if err := RequestDocumentSignatureValidator(s.idService, collaborator).Validate(old, model); err != nil { return nil, errors.NewTypedError(ErrDocumentInvalid, err) } @@ -167,11 +183,6 @@ func (s service) RequestDocumentSignature(ctx context.Context, model Model) (*co srvLog.Infof("document received %x with signing root %x", model.ID(), sr) - tenantID, err := acc.GetIdentityID() - if err != nil { - return nil, err - } - sig, err := acc.SignMsg(sr) if err != nil { return nil, err @@ -180,14 +191,14 @@ func (s service) RequestDocumentSignature(ctx context.Context, model Model) (*co // Logic for receiving version n (n > 1) of the document for the first time // TODO(ved): we should not save the new model with old identifier. We should sync from the peer. - if !s.repo.Exists(tenantID, model.ID()) && !utils.IsSameByteSlice(model.ID(), model.CurrentVersion()) { - err = s.repo.Create(tenantID, model.ID(), model) + if !s.repo.Exists(did[:], model.ID()) && !utils.IsSameByteSlice(model.ID(), model.CurrentVersion()) { + err = s.repo.Create(did[:], model.ID(), model) if err != nil { return nil, errors.NewTypedError(ErrDocumentPersistence, err) } } - err = s.repo.Create(tenantID, model.CurrentVersion(), model) + err = s.repo.Create(did[:], model.CurrentVersion(), model) if err != nil { return nil, errors.NewTypedError(ErrDocumentPersistence, err) } @@ -196,7 +207,7 @@ func (s service) RequestDocumentSignature(ctx context.Context, model Model) (*co return sig, nil } -func (s service) ReceiveAnchoredDocument(ctx context.Context, model Model, senderID []byte) error { +func (s service) ReceiveAnchoredDocument(ctx context.Context, model Model, collaborator identity.DID) error { acc, err := contextutil.Account(ctx) if err != nil { return ErrDocumentConfigAccountID @@ -207,11 +218,22 @@ func (s service) ReceiveAnchoredDocument(ctx context.Context, model Model, sende return err } did := identity.NewDIDFromBytes(idBytes) + if model == nil { - return errors.New("no model given") + return ErrDocumentNil } - if err := PostAnchoredValidator(s.idService, s.anchorRepository).Validate(nil, model); err != nil { + var old Model + // lets pick the old version of the document from the repo and pass this to the validator + if !utils.IsEmptyByteSlice(model.PreviousVersion()) { + old, err = s.repo.Get(did[:], model.PreviousVersion()) + if err != nil { + // TODO(ved): we should pull the old document from the peer + log.Infof("failed to fetch previous document: %v", err) + } + } + + if err := ReceivedAnchoredDocumentValidator(s.idService, s.anchorRepository, collaborator).Validate(old, model); err != nil { return errors.NewTypedError(ErrDocumentInvalid, err) } @@ -220,11 +242,15 @@ func (s service) ReceiveAnchoredDocument(ctx context.Context, model Model, sende return errors.NewTypedError(ErrDocumentPersistence, err) } - ts, _ := ptypes.TimestampProto(time.Now().UTC()) + ts, err := utils.ToTimestamp(time.Now().UTC()) + if err != nil { + return errors.NewTypedError(ErrDocumentNotification, err) + } + notificationMsg := ¬ificationpb.NotificationMessage{ EventType: uint32(notification.ReceivedPayload), AccountId: did.String(), - FromId: hexutil.Encode(senderID), + FromId: hexutil.Encode(collaborator[:]), ToId: did.String(), Recorded: ts, DocumentType: model.DocumentType(), diff --git a/documents/validator.go b/documents/validator.go index ced68f0ed..63d01569f 100644 --- a/documents/validator.go +++ b/documents/validator.go @@ -1,6 +1,8 @@ package documents import ( + "time" + "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" @@ -8,6 +10,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ) +// MaxAuthoredToCommitDuration is the maximum allowed time period for a document to be anchored after a authoring it based on document timestamp. +// I.E. This is basically the maximum time period allowed for document consensus to complete as well. +const MaxAuthoredToCommitDuration = 120 * time.Minute + // Validator is an interface every Validator (atomic or group) should implement type Validator interface { // Validate validates the updates to the model in newState. @@ -19,7 +25,6 @@ type ValidatorGroup []Validator //Validate will execute all group specific atomic validations func (group ValidatorGroup) Validate(oldState Model, newState Model) (errs error) { - for _, v := range group { if err := v.Validate(oldState, newState); err != nil { errs = errors.AppendError(errs, err) @@ -162,6 +167,32 @@ func documentRootValidator() Validator { }) } +// documentAuthorValidator checks if a given sender DID is the document author +func documentAuthorValidator(sender identity.DID) Validator { + return ValidatorFunc(func(_, model Model) error { + if !model.Author().Equal(sender) { + return errors.New("document sender is not the author") + } + + return nil + }) +} + +// documentTimestampForSigningValidator checks if a given document has a timestamp recent enough to be signed +func documentTimestampForSigningValidator() Validator { + return ValidatorFunc(func(_, model Model) error { + tm, err := model.Timestamp() + if err != nil { + return errors.New("failed to get document timestamp: %v", err) + } + + if tm.Before(time.Now().UTC().Add(-MaxAuthoredToCommitDuration)) { + return errors.New("document is too old to be signed") + } + return nil + }) +} + // signaturesValidator validates all the signatures in the core document // assumes signing root is verified // Note: can be used when during the signature request on collaborator side and post signature collection on sender side @@ -178,13 +209,54 @@ func signaturesValidator(idService identity.ServiceDID) Validator { return errors.New("atleast one signature expected") } + collaborators, err := model.GetSignerCollaborators(model.Author()) + if err != nil { + return errors.New("could not get signer collaborators") + } + + authorFound := false for _, sig := range signatures { - if erri := idService.ValidateSignature(&sig, sr); erri != nil { + sigDID := identity.NewDIDFromBytes(sig.SignerId) + if model.Author().Equal(sigDID) { + authorFound = true + } + + // we only care about validating that signer is part of signing collaborators and not the other way around + // since a collaborator can decide to not sign a document and the protocol still defines it as a valid state for a model. + collaboratorFound := false + for _, cb := range collaborators { + if sigDID.Equal(cb) { + collaboratorFound = true + } + } + + // signer is not found in signing collaborators and he is not the author either + if !collaboratorFound && !model.Author().Equal(sigDID) { + err = errors.AppendError( + err, + errors.New("signature_%s verification failed: signer is not part of the signing collaborators", hexutil.Encode(sig.SignerId))) + continue + } + + tm, terr := model.Timestamp() + if terr != nil { + err = errors.AppendError( + err, + errors.New("signature_%s verification failed: %v", hexutil.Encode(sig.SignerId), terr)) + continue + } + + if erri := idService.ValidateSignature(sigDID, sig.PublicKey, sig.Signature, sr, tm); erri != nil { err = errors.AppendError( err, - errors.New("signature_%s verification failed: %v", hexutil.Encode(sig.EntityId), erri)) + errors.New("signature_%s verification failed: %v", hexutil.Encode(sig.SignerId), erri)) } } + if !authorFound { + err = errors.AppendError( + err, + errors.New("signature verification failed: author's signature missing on document")) + } return err }) } @@ -208,7 +280,7 @@ func anchoredValidator(repo anchors.AnchorRepository) Validator { return errors.New("failed to get document root: %v", err) } - gotRoot, err := repo.GetDocumentRootOf(anchorID) + gotRoot, anchoredAt, err := repo.GetAnchorData(anchorID) if err != nil { return errors.New("failed to get document root for anchor %s from chain: %v", anchorID.String(), err) } @@ -217,17 +289,46 @@ func anchoredValidator(repo anchors.AnchorRepository) Validator { return errors.New("mismatched document roots") } + tm, err := model.Timestamp() + if err != nil { + return errors.New("failed to get model update time: %v", err) + } + + if tm.Add(MaxAuthoredToCommitDuration).Before(anchoredAt) { + return errors.New("document was anchored after max allowed time for anchor %s", anchorID.String()) + } + + return nil + }) +} + +// transitionValidator checks that the document changes are within the transition_rule capability of the +// collaborator making the changes +func transitionValidator(collaborator identity.DID) Validator { + return ValidatorFunc(func(old, new Model) error { + if old == nil { + return nil + } + err := old.CollaboratorCanUpdate(new, collaborator) + if err != nil { + return errors.New("invalid document state transition: %v", err) + } return nil }) } // SignatureRequestValidator returns a validator group with following validators +// document timestamp for signing validator +// document author validator // base validator // signing root validator // signatures validator // should be used when node receives a document requesting for signature -func SignatureRequestValidator(idService identity.ServiceDID) ValidatorGroup { - return SignatureValidator(idService) +func SignatureRequestValidator(sender identity.DID, idService identity.ServiceDID) ValidatorGroup { + return ValidatorGroup{ + documentTimestampForSigningValidator(), + documentAuthorValidator(sender), + SignatureValidator(idService)} } // PreAnchorValidator is a validator group with following validators @@ -254,6 +355,30 @@ func PostAnchoredValidator(idService identity.ServiceDID, repo anchors.AnchorRep } } +// ReceivedAnchoredDocumentValidator is a validator group with following validators +// transitionValidator +// PostAnchoredValidator +func ReceivedAnchoredDocumentValidator( + idService identity.ServiceDID, + repo anchors.AnchorRepository, + collaborator identity.DID) ValidatorGroup { + return ValidatorGroup{ + transitionValidator(collaborator), + PostAnchoredValidator(idService, repo), + } +} + +// RequestDocumentSignatureValidator is a validator group with the following validators +// SignatureValidator +// transitionsValidator +// it should be called when a document is received over the p2p layer before signing +func RequestDocumentSignatureValidator(idService identity.ServiceDID, collaborator identity.DID) ValidatorGroup { + return ValidatorGroup{ + transitionValidator(collaborator), + SignatureValidator(idService), + } +} + // SignatureValidator is a validator group with following validators // baseValidator // signingRootValidator diff --git a/documents/validator_test.go b/documents/validator_test.go index aaeea78d5..75e512e61 100644 --- a/documents/validator_test.go +++ b/documents/validator_test.go @@ -4,17 +4,19 @@ package documents import ( "testing" - - "github.com/stretchr/testify/mock" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) type MockValidator struct{} @@ -242,6 +244,25 @@ func TestValidator_documentRootValidator(t *testing.T) { model.AssertExpectations(t) } +func TestValidator_TransitionValidator(t *testing.T) { + id1 := testingidentity.GenerateRandomDID() + updated := new(mockModel) + + // does not error out if there is no old document model (if new model is the first version of the document model) + tv := transitionValidator(id1) + err := tv.Validate(nil, updated) + assert.NoError(t, err) + + old := new(mockModel) + old.On("CollaboratorCanUpdate", updated, id1).Return(errors.New("error")) + err = tv.Validate(old, updated) + assert.Contains(t, err.Error(), "invalid document state transition: error") + + old.On("CollaboratorCanUpdate", updated, id1).Return(nil) + err = tv.Validate(old.Model, updated) + assert.NoError(t, err) +} + func TestValidator_SignatureValidator(t *testing.T) { account, err := contextutil.Account(testingconfig.CreateAccountContext(t, cfg)) assert.NoError(t, err) @@ -253,7 +274,7 @@ func TestValidator_SignatureValidator(t *testing.T) { model.On("ID").Return(utils.RandomSlice(32)) model.On("CurrentVersion").Return(utils.RandomSlice(32)) model.On("NextVersion").Return(utils.RandomSlice(32)) - idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) model.On("CalculateSigningRoot").Return(nil, errors.New("error")) err = sv.Validate(nil, model) assert.Error(t, err) @@ -273,12 +294,21 @@ func TestValidator_SignatureValidator(t *testing.T) { assert.Contains(t, err.Error(), "atleast one signature expected") // mismatch + tm := time.Now() s := &coredocumentpb.Signature{ Signature: utils.RandomSlice(32), - EntityId: utils.RandomSlice(6), + SignerId: utils.RandomSlice(identity.DIDLength), PublicKey: utils.RandomSlice(32), } + s2 := &coredocumentpb.Signature{ + Signature: utils.RandomSlice(32), + SignerId: utils.RandomSlice(identity.DIDLength), + PublicKey: utils.RandomSlice(32), + } + + did1 := identity.NewDIDFromBytes(s.SignerId) + idService = new(testingcommons.MockIdentityService) sv = SignatureValidator(idService) model = new(mockModel) @@ -286,7 +316,10 @@ func TestValidator_SignatureValidator(t *testing.T) { model.On("CurrentVersion").Return(utils.RandomSlice(32)) model.On("NextVersion").Return(utils.RandomSlice(32)) model.On("CalculateSigningRoot").Return(sr, nil) - idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(errors.New("invalid signature")).Once() + model.On("Author").Return(did1) + model.On("Timestamp").Return(tm, nil) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("invalid signature")).Once() model.On("Signatures").Return().Once() model.sigs = append(model.sigs, s) err = sv.Validate(nil, model) @@ -294,15 +327,83 @@ func TestValidator_SignatureValidator(t *testing.T) { assert.Error(t, err) assert.Equal(t, 1, errors.Len(err)) + // model author not found + idService = new(testingcommons.MockIdentityService) + sv = SignatureValidator(idService) + model = new(mockModel) + model.On("ID").Return(utils.RandomSlice(32)) + model.On("CurrentVersion").Return(utils.RandomSlice(32)) + model.On("NextVersion").Return(utils.RandomSlice(32)) + model.On("CalculateSigningRoot").Return(sr, nil) + model.On("Author").Return(testingidentity.GenerateRandomDID()) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + model.On("Signatures").Return().Once() + model.sigs = append(model.sigs, s) + err = sv.Validate(nil, model) + model.AssertExpectations(t) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + assert.Contains(t, err.Error(), "author's signature missing on document") + + // signer not part of signing collaborators + idService = new(testingcommons.MockIdentityService) + sv = SignatureValidator(idService) + model = new(mockModel) + model.On("ID").Return(utils.RandomSlice(32)) + model.On("CurrentVersion").Return(utils.RandomSlice(32)) + model.On("NextVersion").Return(utils.RandomSlice(32)) + model.On("CalculateSigningRoot").Return(sr, nil) + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + model.On("Signatures").Return().Once() + model.sigs = append(model.sigs, s, s2) + err = sv.Validate(nil, model) + model.AssertExpectations(t) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + assert.Contains(t, err.Error(), "signer is not part of the signing collaborators") + + // model timestamp err + idService = new(testingcommons.MockIdentityService) + sv = SignatureValidator(idService) + model = new(mockModel) + model.On("ID").Return(utils.RandomSlice(32)) + model.On("CurrentVersion").Return(utils.RandomSlice(32)) + model.On("NextVersion").Return(utils.RandomSlice(32)) + model.On("CalculateSigningRoot").Return(sr, nil) + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, errors.New("some timestamp error")) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + model.On("Signatures").Return().Once() + model.sigs = append(model.sigs, s) + err = sv.Validate(nil, model) + model.AssertExpectations(t) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + assert.Contains(t, err.Error(), "some timestamp error") + // success + idService = new(testingcommons.MockIdentityService) + sv = SignatureValidator(idService) s, err = account.SignMsg(sr) assert.NoError(t, err) + acID, err := account.GetIdentityID() + assert.NoError(t, err) + did1 = identity.NewDIDFromBytes(acID) model = new(mockModel) model.On("ID").Return(utils.RandomSlice(32)) model.On("CurrentVersion").Return(utils.RandomSlice(32)) model.On("NextVersion").Return(utils.RandomSlice(32)) model.On("CalculateSigningRoot").Return(sr, nil) - idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() + model.On("Author").Return(did1) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did1, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) + idService.On("ValidateSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() model.On("Signatures").Return().Once() model.sigs = append(model.sigs, s) err = sv.Validate(nil, model) @@ -332,17 +433,22 @@ func TestValidator_signatureValidator(t *testing.T) { assert.Contains(t, err.Error(), "atleast one signature expected") // failed validation + tm := time.Now().UTC() s := &coredocumentpb.Signature{ Signature: utils.RandomSlice(32), - EntityId: utils.RandomSlice(6), + SignerId: utils.RandomSlice(identity.DIDLength), PublicKey: utils.RandomSlice(32), } + did := identity.NewDIDFromBytes(s.SignerId) model = new(mockModel) model.On("CalculateSigningRoot").Return(sr, nil).Once() model.On("Signatures").Return().Once() + model.On("Author").Return(did) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, s) srv = new(testingcommons.MockIdentityService) - srv.On("ValidateSignature", s, sr).Return(errors.New("error")).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(s.SignerId), s.PublicKey, s.Signature, sr, tm).Return(errors.New("error")).Once() ssv = signaturesValidator(srv) err = ssv.Validate(nil, model) model.AssertExpectations(t) @@ -354,9 +460,12 @@ func TestValidator_signatureValidator(t *testing.T) { model = new(mockModel) model.On("CalculateSigningRoot").Return(sr, nil).Once() model.On("Signatures").Return().Once() + model.On("Author").Return(did) + model.On("GetSignerCollaborators", mock.Anything).Return([]identity.DID{did, testingidentity.GenerateRandomDID()}, nil) + model.On("Timestamp").Return(tm, nil) model.sigs = append(model.sigs, s) srv = new(testingcommons.MockIdentityService) - srv.On("ValidateSignature", s, sr).Return(nil).Once() + srv.On("ValidateSignature", identity.NewDIDFromBytes(s.SignerId), s.PublicKey, s.Signature, sr, tm).Return(nil).Once() ssv = signaturesValidator(srv) err = ssv.Validate(nil, model) model.AssertExpectations(t) @@ -403,7 +512,7 @@ func TestValidator_anchoredValidator(t *testing.T) { assert.Nil(t, err) r := &mockRepo{} av = anchoredValidator(r) - r.On("GetDocumentRootOf", anchorID).Return(nil, errors.New("error")).Once() + r.On("GetAnchorData", anchorID).Return(nil, time.Now(), errors.New("error")).Once() model = new(mockModel) model.On("CurrentVersion").Return(anchorID[:]).Once() model.On("CalculateDocumentRoot").Return(utils.RandomSlice(32), nil).Once() @@ -417,7 +526,7 @@ func TestValidator_anchoredValidator(t *testing.T) { docRoot := anchors.RandomDocumentRoot() r = &mockRepo{} av = anchoredValidator(r) - r.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() + r.On("GetAnchorData", anchorID).Return(docRoot, time.Now(), nil).Once() model = new(mockModel) model.On("CurrentVersion").Return(anchorID[:]).Once() model.On("CalculateDocumentRoot").Return(utils.RandomSlice(32), nil).Once() @@ -427,13 +536,29 @@ func TestValidator_anchoredValidator(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "mismatched document roots") + // anchored after max allowed time + r = &mockRepo{} + av = anchoredValidator(r) + tm := time.Now() + r.On("GetAnchorData", anchorID).Return(docRoot, tm, nil).Once() + model = new(mockModel) + model.On("CurrentVersion").Return(anchorID[:]).Once() + model.On("CalculateDocumentRoot").Return(docRoot[:], nil).Once() + model.On("Timestamp").Return(tm.Add(-MaxAuthoredToCommitDuration-1), nil).Once() + err = av.Validate(nil, model) + model.AssertExpectations(t) + r.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "document was anchored after max allowed time for anchor") + // success r = &mockRepo{} av = anchoredValidator(r) - r.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() + r.On("GetAnchorData", anchorID).Return(docRoot, time.Now(), nil).Once() model = new(mockModel) model.On("CurrentVersion").Return(anchorID[:]).Once() model.On("CalculateDocumentRoot").Return(docRoot[:], nil).Once() + model.On("Timestamp").Return(time.Now(), nil).Once() err = av.Validate(nil, model) model.AssertExpectations(t) r.AssertExpectations(t) @@ -446,6 +571,46 @@ func TestPostAnchoredValidator(t *testing.T) { } func TestSignatureRequestValidator(t *testing.T) { - srv := SignatureRequestValidator(nil) + srv := SignatureRequestValidator(testingidentity.GenerateRandomDID(), nil) assert.Len(t, srv, 3) + +} + +func TestDocumentAuthorValidator(t *testing.T) { + did := testingidentity.GenerateRandomDID() + av := documentAuthorValidator(did) + + // fail + model := new(mockModel) + model.On("Author").Return(testingidentity.GenerateRandomDID()).Once() + err := av.Validate(nil, model) + model.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "document sender is not the author") + + // success + model = new(mockModel) + model.On("Author").Return(did).Once() + err = av.Validate(nil, model) + model.AssertExpectations(t) + assert.Nil(t, err) +} + +func TestDocumentTimestampForSigningValidator(t *testing.T) { + av := documentTimestampForSigningValidator() + + // fail + model := new(mockModel) + model.On("Timestamp").Return(time.Now().UTC().Add(-MaxAuthoredToCommitDuration), nil).Once() + err := av.Validate(nil, model) + model.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "document is too old to be signed") + + // success + model = new(mockModel) + model.On("Timestamp").Return(time.Now().UTC(), nil).Once() + err = av.Validate(nil, model) + model.AssertExpectations(t) + assert.Nil(t, err) } diff --git a/documents/write_acls.go b/documents/write_acls.go index 6e8cfbb1b..6d976cf70 100644 --- a/documents/write_acls.go +++ b/documents/write_acls.go @@ -3,24 +3,30 @@ package documents import ( "bytes" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs" ) -// changedField holds the compact property, old and new value of the field that is changed +// ChangedField holds the compact property, old and new value of the field that is changed // if the old is nil, then it is a set operation // if new is nil, then it is an unset operation // if both old and new are set, then it is an edit operation -type changedField struct { - property, old, new []byte +type ChangedField struct { + Property, Old, New []byte + Name string } -// getChangedFields takes two document trees and returns the compact value, old and new value of the fields that are changed in new tree. +// GetChangedFields takes two document trees and returns the compact property, old and new value of the fields that are changed in new tree. // Properties may have been added to the new tree or removed from the new tree. // In Either case, since the new tree is different from old, that is considered a change. -func getChangedFields(oldTree, newTree *proofs.DocumentTree, lengthSuffix string) (changedFields []changedField) { +func GetChangedFields(oldTree, newTree *proofs.DocumentTree, lengthSuffix string) (changedFields []ChangedField) { oldProps := oldTree.PropertyOrder() newProps := newTree.PropertyOrder() + // check each property and append it changed fields if the value is different. props := make(map[string]proofs.Property) for _, p := range append(oldProps, newProps...) { // we can ignore the length property since any change in slice or map will return in addition or deletion of properties in the new tree @@ -28,17 +34,14 @@ func getChangedFields(oldTree, newTree *proofs.DocumentTree, lengthSuffix string continue } - if _, ok := props[p.ReadableName()]; ok { + pn := p.ReadableName() + if _, ok := props[pn]; ok { continue } - props[p.ReadableName()] = p - } - - // check each property and append it changed fields if the value is different. - for k, p := range props { - _, ol := oldTree.GetLeafByProperty(k) - _, nl := newTree.GetLeafByProperty(k) + props[pn] = p + _, ol := oldTree.GetLeafByProperty(pn) + _, nl := newTree.GetLeafByProperty(pn) if ol == nil { changedFields = append(changedFields, newChangedField(p, nl, false)) @@ -58,10 +61,11 @@ func getChangedFields(oldTree, newTree *proofs.DocumentTree, lengthSuffix string } if !bytes.Equal(ov, nv) { - changedFields = append(changedFields, changedField{ - property: p.CompactName(), - old: ov, - new: nv, + changedFields = append(changedFields, ChangedField{ + Name: pn, + Property: p.CompactName(), + Old: ov, + New: nv, }) } } @@ -69,18 +73,174 @@ func getChangedFields(oldTree, newTree *proofs.DocumentTree, lengthSuffix string return changedFields } -func newChangedField(p proofs.Property, leaf *proofs.LeafNode, old bool) changedField { +func newChangedField(p proofs.Property, leaf *proofs.LeafNode, old bool) ChangedField { v := leaf.Value if leaf.Hashed { v = leaf.Hash } - cf := changedField{property: p.CompactName()} + cf := ChangedField{Property: p.CompactName(), Name: p.ReadableName()} if old { - cf.old = v + cf.Old = v return cf } - cf.new = v + cf.New = v return cf } + +// TransitionRulesFor returns a copy all the transition rules for the DID. +func (cd *CoreDocument) TransitionRulesFor(did identity.DID) (rules []coredocumentpb.TransitionRule) { + for _, rule := range cd.Document.TransitionRules { + for _, rk := range rule.Roles { + role, err := getRole(rk, cd.Document.Roles) + if err != nil { + continue + } + + if _, ok := isDIDInRole(role, did); !ok { + continue + } + + rules = append(rules, coredocumentpb.TransitionRule{ + RuleKey: copyBytes(rule.RuleKey), + Roles: copyByteSlice(rule.Roles), + MatchType: rule.MatchType, + Field: copyBytes(rule.Field), + Action: rule.Action, + }) + } + } + + return rules +} + +func copyBytes(data []byte) []byte { + if data == nil { + return nil + } + + nb := make([]byte, len(data), len(data)) + copy(nb, data) + return nb +} + +func copyByteSlice(data [][]byte) [][]byte { + nbs := make([][]byte, len(data), len(data)) + for i, b := range data { + nbs[i] = copyBytes(b) + } + + return nbs +} + +// ValidateTransitions validates the changedFields based on the rules provided. +// returns an error if any ChangedField violates the rules. +func ValidateTransitions(rules []coredocumentpb.TransitionRule, changedFields []ChangedField) error { + cfMap := make(map[string]struct{}) + for _, cf := range changedFields { + cfMap[cf.Name] = struct{}{} + } + + for _, rule := range rules { + for _, cf := range changedFields { + if isValidTransition(rule, cf) { + delete(cfMap, cf.Name) + } + } + } + + if len(cfMap) == 0 { + return nil + } + + var err error + for k := range cfMap { + err = errors.AppendError(err, errors.New("invalid transition: %s", k)) + } + + return err +} + +func isValidTransition(rule coredocumentpb.TransitionRule, cf ChangedField) bool { + // changed property length should be at least equal to rule property + if len(cf.Property) < len(rule.Field) { + return false + } + + // if the match type is prefix, get the compact property till prefix + v := cf.Property + if rule.MatchType == coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX { + v = v[:len(rule.Field)] + } + + // check the properties are equal + if !bytes.Equal(v, rule.Field) { + return false + } + + // check if the action is allowed + // for now, we have only edit action + // edit allows following + // 1. update: editing a value + // 2. set: setting a new value ex: adding to slice or map + // 3. delete: deleting the new value ex: removing from slice or map + // Once we have more actions, like set, increment etc.. we can do those checks here + return true +} + +// CollaboratorCanUpdate validates the changes made by the collaborator in the new document. +// returns error if the transitions are not allowed for the collaborator. +func (cd *CoreDocument) CollaboratorCanUpdate(ncd *CoreDocument, collaborator identity.DID, docType string) error { + oldTree, err := cd.documentTree(docType) + if err != nil { + return err + } + + newTree, err := ncd.documentTree(docType) + if err != nil { + return err + } + + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + rules := cd.TransitionRulesFor(collaborator) + return ValidateTransitions(rules, cf) +} + +// initTransitionRules initiates the transition rules for a given Core Document. +// Collaborators are given default edit capability over all fields of the CoreDocument and underlying documents such as invoices or purchase orders. +// if the rules are created already, this is a no-op. +// if collaborators are empty, it is a no-op +func (cd *CoreDocument) initTransitionRules(collaborators []identity.DID, documentPrefix []byte) { + if len(cd.Document.Roles) > 0 && len(cd.Document.TransitionRules) > 0 { + return + } + if len(collaborators) < 0 { + return + } + cd.addCollaboratorsToTransitionRules(collaborators, documentPrefix) +} + +// addCollaboratorsToTransitionRules adds the given collaborators to a new transition rule which defaults to +// granting edit capability over all fields of the document. +func (cd *CoreDocument) addCollaboratorsToTransitionRules(collaborators []identity.DID, documentPrefix []byte) { + role := newRoleWithCollaborators(collaborators) + if role == nil { + return + } + cd.Document.Roles = append(cd.Document.Roles, role) + cd.addNewTransitionRule(role.RoleKey, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX, compactProperties(CDTreePrefix), coredocumentpb.TransitionAction_TRANSITION_ACTION_EDIT) + cd.addNewTransitionRule(role.RoleKey, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX, documentPrefix, coredocumentpb.TransitionAction_TRANSITION_ACTION_EDIT) +} + +// addNewTransitionRule creates a new transition rule with the given parameters. +func (cd *CoreDocument) addNewTransitionRule(roleKey []byte, matchType coredocumentpb.FieldMatchType, field []byte, action coredocumentpb.TransitionAction) { + rule := &coredocumentpb.TransitionRule{ + RuleKey: utils.RandomSlice(32), + MatchType: matchType, + Action: action, + Field: field, + Roles: [][]byte{roleKey}, + } + cd.Document.TransitionRules = append(cd.Document.TransitionRules, rule) +} diff --git a/documents/write_acls_test.go b/documents/write_acls_test.go index 19dd848d4..94891c95b 100644 --- a/documents/write_acls_test.go +++ b/documents/write_acls_test.go @@ -4,12 +4,15 @@ package documents import ( "crypto/sha256" - "fmt" "reflect" "testing" "time" + "github.com/centrifuge/centrifuge-protobufs/documenttypes" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/invoice" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs" @@ -27,12 +30,12 @@ func TestWriteACLs_getChangedFields_different_types(t *testing.T) { Currency: "EUR", } - oldTree := getTree(t, &ocd) - newTree := getTree(t, &ncd) + oldTree := getTree(t, &ocd, "", nil) + newTree := getTree(t, &ncd, "", nil) - cf := getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) // cf length should be len(ocd) and len(ncd) = 31 changed field - assert.Len(t, cf, 31) + assert.Len(t, cf, 33) } @@ -40,24 +43,24 @@ func TestWriteACLs_getChangedFields_same_document(t *testing.T) { cd, err := newCoreDocument() assert.NoError(t, err) ocd := cd.Document - oldTree := getTree(t, &ocd) - newTree := getTree(t, &ocd) - cf := getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree := getTree(t, &ocd, "", nil) + newTree := getTree(t, &ocd, "", nil) + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 0) // check hashed field ocd.PreviousRoot = utils.RandomSlice(32) - oldTree = getTree(t, &ocd) - newTree = getTree(t, &ocd) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree = getTree(t, &ocd, "", nil) + newTree = getTree(t, &ocd, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 0) } -func testExpectedProps(t *testing.T, cf []changedField, eprops map[string]struct{}) { +func testExpectedProps(t *testing.T, cf []ChangedField, eprops map[string]struct{}) { for _, f := range cf { - _, ok := eprops[hexutil.Encode(f.property)] + _, ok := eprops[hexutil.Encode(f.Property)] if !ok { - assert.Failf(t, "", "expected %x property to be present", f.property) + assert.Failf(t, "", "expected %x property to be present", f.Property) } } } @@ -66,24 +69,32 @@ func TestWriteACLs_getChangedFields_with_core_document(t *testing.T) { doc, err := newCoreDocument() assert.NoError(t, err) doc.Document.DocumentRoot = utils.RandomSlice(32) - ndoc, err := doc.PrepareNewVersion([]string{testingidentity.GenerateRandomDID().String()}, true) + ndoc, err := doc.PrepareNewVersion([]string{testingidentity.GenerateRandomDID().String()}, true, []byte("po")) assert.NoError(t, err) // preparing new version would have changed the following properties + // current_version // previous_version // next_version // previous_root - // roles // current pre image // next pre image + // read_rules.roles // read_rules.action - oldTree := getTree(t, &doc.Document) - newTree := getTree(t, &ndoc.Document) - cf := getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) - assert.Len(t, cf, 9) + // transition_rules.RuleKey + // (transition_rules.Roles + // transition_rules.MatchType + // transition_rules.Action + // transition_rules.Field) x 2 + // roles + 2 + oldTree := getTree(t, &doc.Document, "", nil) + newTree := getTree(t, &ndoc.Document, "", nil) + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + assert.Len(t, cf, 20) rprop := append(ndoc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + rprop2 := append(ndoc.Document.Roles[1].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) eprops := map[string]struct{}{ hexutil.Encode([]byte{0, 0, 0, 4}): {}, hexutil.Encode([]byte{0, 0, 0, 3}): {}, @@ -93,7 +104,18 @@ func TestWriteACLs_getChangedFields_with_core_document(t *testing.T) { hexutil.Encode([]byte{0, 0, 0, 23}): {}, hexutil.Encode([]byte{0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, hexutil.Encode([]byte{0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}): {}, hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop...)): {}, + hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop2...)): {}, } testExpectedProps(t, cf, eprops) @@ -104,13 +126,15 @@ func TestWriteACLs_getChangedFields_with_core_document(t *testing.T) { // previous_version // next_version // previous_root + // current pre image + // next pre image doc = ndoc doc.Document.DocumentRoot = utils.RandomSlice(32) - ndoc, err = doc.PrepareNewVersion(nil, true) + ndoc, err = doc.PrepareNewVersion(nil, true, []byte("po")) assert.NoError(t, err) - oldTree = getTree(t, &doc.Document) - newTree = getTree(t, &ndoc.Document) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree = getTree(t, &doc.Document, "", nil) + newTree = getTree(t, &ndoc.Document, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 6) eprops = map[string]struct{}{ hexutil.Encode([]byte{0, 0, 0, 4}): {}, @@ -129,52 +153,85 @@ func TestWriteACLs_getChangedFields_with_core_document(t *testing.T) { // previous version // next version // previous_root - // roles (new doc will have empty role while old one has one role) + // current pre image + // next pre image + // roles (new doc will have empty role while old one has two roles) // read_rules (new doc will have empty read_rules while old one has read_rules) + // transition_rules (new doc will have empty transition_rules while old one has 2 transition_rules) doc = ndoc ndoc, err = newCoreDocument() assert.NoError(t, err) - oldTree = getTree(t, &doc.Document) - newTree = getTree(t, &ndoc.Document) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) - assert.Len(t, cf, 10) + oldTree = getTree(t, &doc.Document, "", nil) + newTree = getTree(t, &ndoc.Document, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + assert.Len(t, cf, 21) rprop = append(doc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + rprop2 = append(doc.Document.Roles[1].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) eprops = map[string]struct{}{ - hexutil.Encode([]byte{0, 0, 0, 9}): {}, - hexutil.Encode([]byte{0, 0, 0, 4}): {}, - hexutil.Encode([]byte{0, 0, 0, 3}): {}, - hexutil.Encode([]byte{0, 0, 0, 16}): {}, - hexutil.Encode([]byte{0, 0, 0, 2}): {}, - hexutil.Encode([]byte{0, 0, 0, 22}): {}, - hexutil.Encode([]byte{0, 0, 0, 23}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5}): {}, + hexutil.Encode([]byte{0, 0, 0, 9}): {}, + hexutil.Encode([]byte{0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 16}): {}, + hexutil.Encode([]byte{0, 0, 0, 2}): {}, + hexutil.Encode([]byte{0, 0, 0, 22}): {}, + hexutil.Encode([]byte{0, 0, 0, 23}): {}, hexutil.Encode([]byte{0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, hexutil.Encode([]byte{0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}): {}, hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop...)): {}, + hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop2...)): {}, } testExpectedProps(t, cf, eprops) // add different roles and read rules and check + // this will change + // current version + // previous version + // next version + // previous_root + // current pre image + // next pre image + // roles (new doc will have 2 new roles different from 2 old roles) + // read_rules + // transition_rules ndoc.Document.DocumentRoot = utils.RandomSlice(32) - ndoc, err = ndoc.PrepareNewVersion([]string{testingidentity.GenerateRandomDID().String()}, true) + ndoc, err = ndoc.PrepareNewVersion([]string{testingidentity.GenerateRandomDID().String()}, true, []byte("po")) assert.NoError(t, err) - oldTree = getTree(t, &doc.Document) - newTree = getTree(t, &ndoc.Document) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) - assert.Len(t, cf, 10) - fmt.Println(cf) - rprop = append(ndoc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) - rprop2 := append(doc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + oldTree = getTree(t, &doc.Document, "", nil) + newTree = getTree(t, &ndoc.Document, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + assert.Len(t, cf, 16) + rprop = append(doc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + rprop2 = append(doc.Document.Roles[1].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + rprop3 := append(ndoc.Document.Roles[0].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) + rprop4 := append(ndoc.Document.Roles[1].RoleKey, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0) eprops = map[string]struct{}{ - hexutil.Encode([]byte{0, 0, 0, 9}): {}, - hexutil.Encode([]byte{0, 0, 0, 4}): {}, - hexutil.Encode([]byte{0, 0, 0, 3}): {}, - hexutil.Encode([]byte{0, 0, 0, 16}): {}, - hexutil.Encode([]byte{0, 0, 0, 2}): {}, - hexutil.Encode([]byte{0, 0, 0, 22}): {}, - hexutil.Encode([]byte{0, 0, 0, 23}): {}, + hexutil.Encode([]byte{0, 0, 0, 9}): {}, + hexutil.Encode([]byte{0, 0, 0, 4}): {}, + hexutil.Encode([]byte{0, 0, 0, 3}): {}, + hexutil.Encode([]byte{0, 0, 0, 16}): {}, + hexutil.Encode([]byte{0, 0, 0, 2}): {}, + hexutil.Encode([]byte{0, 0, 0, 22}): {}, + hexutil.Encode([]byte{0, 0, 0, 23}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4}): {}, hexutil.Encode([]byte{0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, + hexutil.Encode([]byte{0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}): {}, hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop...)): {}, hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop2...)): {}, + hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop3...)): {}, + hexutil.Encode(append([]byte{0, 0, 0, 1}, rprop4...)): {}, } testExpectedProps(t, cf, eprops) } @@ -192,9 +249,9 @@ func TestWriteACLs_getChangedFields_invoice_document(t *testing.T) { DueDate: dueDate, } - oldTree := getTree(t, doc) - newTree := getTree(t, doc) - cf := getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree := getTree(t, doc, "", nil) + newTree := getTree(t, doc, "", nil) + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 0) // updated doc @@ -207,28 +264,30 @@ func TestWriteACLs_getChangedFields_invoice_document(t *testing.T) { Currency: "EUR", // new field } - oldTree = getTree(t, doc) - newTree = getTree(t, ndoc) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree = getTree(t, doc, "", nil) + newTree = getTree(t, ndoc, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 2) - eprops := map[string]changedField{ + eprops := map[string]ChangedField{ hexutil.Encode([]byte{0, 0, 0, 1}): { - property: []byte{0, 0, 0, 1}, - old: []byte{49, 50, 51, 52, 53}, - new: []byte{49, 50, 51, 52, 53, 54}, + Property: []byte{0, 0, 0, 1}, + Name: "invoice_number", + Old: []byte{49, 50, 51, 52, 53}, + New: []byte{49, 50, 51, 52, 53, 54}, }, hexutil.Encode([]byte{0, 0, 0, 13}): { - property: []byte{0, 0, 0, 13}, - old: []byte{}, - new: []byte{69, 85, 82}, + Property: []byte{0, 0, 0, 13}, + Name: "currency", + Old: []byte{}, + New: []byte{69, 85, 82}, }, } for _, f := range cf { - ef, ok := eprops[hexutil.Encode(f.property)] + ef, ok := eprops[hexutil.Encode(f.Property)] if !ok { - t.Fatalf("expected %x property change", f.property) + t.Fatalf("expected %x property change", f.Property) } assert.True(t, reflect.DeepEqual(f, ef)) @@ -237,9 +296,9 @@ func TestWriteACLs_getChangedFields_invoice_document(t *testing.T) { // completely new doc // this should give 5 property changes ndoc = new(invoicepb.InvoiceData) - oldTree = getTree(t, doc) - newTree = getTree(t, ndoc) - cf = getChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + oldTree = getTree(t, doc, "", nil) + newTree = getTree(t, ndoc, "", nil) + cf = GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) assert.Len(t, cf, 5) eprps := map[string]struct{}{ hexutil.Encode([]byte{0, 0, 0, 1}): {}, @@ -251,8 +310,16 @@ func TestWriteACLs_getChangedFields_invoice_document(t *testing.T) { testExpectedProps(t, cf, eprps) } -func getTree(t *testing.T, doc proto.Message) *proofs.DocumentTree { +func getTree(t *testing.T, doc proto.Message, prefix string, compact []byte) *proofs.DocumentTree { + var prop proofs.Property + if prefix != "" { + prop = proofs.Property{ + Text: prefix, + Compact: compact, + } + } tr := proofs.NewDocumentTree(proofs.TreeOptions{ + ParentPrefix: prop, CompactProperties: true, EnableHashSorting: true, SaltsLengthSuffix: proofs.DefaultSaltsLengthSuffix, @@ -264,3 +331,247 @@ func getTree(t *testing.T, doc proto.Message) *proofs.DocumentTree { assert.NoError(t, tree.Generate()) return tree } + +func TestCoreDocument_transitionRuleForAccount(t *testing.T) { + doc, err := newCoreDocument() + assert.NoError(t, err) + id := testingidentity.GenerateRandomDID() + rules := doc.TransitionRulesFor(id) + assert.Len(t, rules, 0) + + // add roles and rules + _, rule := createTransitionRules(t, doc, id, nil, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX) + rules = doc.TransitionRulesFor(id) + assert.Len(t, rules, 1) + assert.Equal(t, *rule, rules[0]) + + // wrong id + rules = doc.TransitionRulesFor(testingidentity.GenerateRandomDID()) + assert.Len(t, rules, 0) +} + +func createTransitionRules(t *testing.T, doc *CoreDocument, id identity.DID, field []byte, matchType coredocumentpb.FieldMatchType) (*coredocumentpb.Role, *coredocumentpb.TransitionRule) { + role := newRole() + role.Collaborators = append(role.Collaborators, id[:]) + rule := &coredocumentpb.TransitionRule{ + RuleKey: utils.RandomSlice(32), + Roles: [][]byte{role.RoleKey}, + Field: field, + MatchType: matchType, + Action: coredocumentpb.TransitionAction_TRANSITION_ACTION_EDIT, + } + doc.Document.TransitionRules = append(doc.Document.TransitionRules, rule) + doc.Document.Roles = append(doc.Document.Roles, role) + return role, rule +} + +func prepareDocument(t *testing.T) (*CoreDocument, identity.DID, identity.DID, string) { + doc, err := newCoreDocument() + assert.NoError(t, err) + doc.Document.DocumentRoot = utils.RandomSlice(32) + docType := documenttypes.InvoiceDataTypeUrl + id1 := testingidentity.GenerateRandomDID() + id2 := testingidentity.GenerateRandomDID() + + // id1 will have rights to update all the fields in the core document + createTransitionRules(t, doc, id1, compactProperties(CDTreePrefix), coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX) + + // id2 will have write access to only identifiers + // id2 is the bad actor + fields := [][]byte{ + {0, 0, 0, 4}, + {0, 0, 0, 3}, + {0, 0, 0, 16}, + {0, 0, 0, 2}, + {0, 0, 0, 22}, + {0, 0, 0, 23}, + } + + for _, f := range fields { + createTransitionRules(t, doc, id2, append(compactProperties(CDTreePrefix), f...), coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_EXACT) + } + + return doc, id1, id2, docType +} + +func TestWriteACLs_validateTransitions_roles_read_rules(t *testing.T) { + doc, id1, id2, docType := prepareDocument(t) + + // prepare a new version of the document with out collaborators + ndoc, err := doc.PrepareNewVersion(nil, true, []byte("invoice")) + assert.NoError(t, err) + + // if this was changed by the id1, everything should be fine + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id1, docType)) + + // if this was changed by id2, it should still be okay since roles would not have changed + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id2, docType)) + + // prepare the new document with a new collaborator, this will trigger read_rules and roles update + ndoc, err = doc.PrepareNewVersion([]string{testingidentity.GenerateRandomDID().String()}, true, []byte("invoice")) + assert.NoError(t, err) + + // should not error out if the change was done by id1 + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id1, docType)) + + // this should fail since id2 has no write permission to roles, read_rules, and transition rules + err = doc.CollaboratorCanUpdate(ndoc, id2, docType) + assert.Error(t, err) + // we should have 3 errors + // 1. update to roles + // 2. update to read_rules + // 3. update to read_rules action + assert.Equal(t, 14, errors.Len(err)) + + // check with some random collaborator who has no permission at all + err = doc.CollaboratorCanUpdate(ndoc, testingidentity.GenerateRandomDID(), docType) + assert.Error(t, err) + // error should all have field changes + // all the identifier changes = 6 + // role changes = 2 + // read_rule changes = 2 + // transition rule changes = 10 + // total = 9 + assert.Equal(t, 20, errors.Len(err)) +} + +func TestWriteACLs_validate_transitions_nfts(t *testing.T) { + doc, id1, id2, docType := prepareDocument(t) + + // update nfts alone check for validation + // this should only change nfts + registry := testingidentity.GenerateRandomDID() + ndoc, err := doc.AddNFT(false, registry.ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + + // if id1 changed it, it should be okay + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id1, docType)) + + // if id2 made the change, it should error out with one invalid transition + err = doc.CollaboratorCanUpdate(ndoc, id2, docType) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + + // add a specific rule that allow id2 to update specific nft registry + field := append(registry.ToAddress().Bytes(), make([]byte, 12, 12)...) + field = append(compactProperties(CDTreePrefix), append([]byte{0, 0, 0, 20}, field...)...) + createTransitionRules(t, doc, id2, field, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_EXACT) + ndoc, err = doc.AddNFT(false, registry.ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + + // if id1 changed it, it should be okay + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id1, docType)) + + // if id2 should be okay since we added a specific registry + assert.NoError(t, doc.CollaboratorCanUpdate(ndoc, id2, docType)) + + // id2 went rogue and updated nft for different registry + registry2 := testingidentity.GenerateRandomDID() + ndoc.Document.DocumentRoot = utils.RandomSlice(32) + ndoc1, err := ndoc.AddNFT(false, registry2.ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + + // if id1 changed it, it should be okay + assert.NoError(t, ndoc.CollaboratorCanUpdate(ndoc1, id1, docType)) + + // if id2 is allowed to change only nft with specific registry + // this should trigger 1 error + err = ndoc.CollaboratorCanUpdate(ndoc1, id2, docType) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + + // add a rule for id2 that will allow any nft update + field = append(compactProperties(CDTreePrefix), []byte{0, 0, 0, 20}...) + createTransitionRules(t, ndoc1, id2, field, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX) + + ndoc1.Document.DocumentRoot = utils.RandomSlice(32) + ndoc2, err := ndoc1.AddNFT(false, testingidentity.GenerateRandomDID().ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + + // id1 change should be fine + assert.NoError(t, ndoc1.CollaboratorCanUpdate(ndoc2, id1, docType)) + + // id2 change should be fine since id2 has a rule allowing nft update + assert.NoError(t, ndoc1.CollaboratorCanUpdate(ndoc2, id2, docType)) + + // now make a change that will trigger read rules and roles as well + ndoc2, err = ndoc1.AddNFT(true, testingidentity.GenerateRandomDID().ToAddress(), utils.RandomSlice(32)) + assert.NoError(t, err) + + // id1 change should be fine + assert.NoError(t, ndoc1.CollaboratorCanUpdate(ndoc2, id1, docType)) + + // id2 change will be invalid since with grant access, roles and read_rules will be updated + // this will lead to 3 errors + // 1. roles + // 2. read_rules.roles + // 3. read_rules.action + err = ndoc1.CollaboratorCanUpdate(ndoc2, id2, docType) + assert.Error(t, err) + assert.Equal(t, 3, errors.Len(err)) +} + +func testInvoiceChange(t *testing.T, cd *CoreDocument, id identity.DID, doc1, doc2 proto.Message, prefix string, compact []byte) error { + oldTree := getTree(t, doc1, prefix, compact) + newTree := getTree(t, doc2, prefix, compact) + + cf := GetChangedFields(oldTree, newTree, proofs.DefaultSaltsLengthSuffix) + rules := cd.TransitionRulesFor(id) + return ValidateTransitions(rules, cf) +} + +func TestWriteACLs_validTransitions_invoice_data(t *testing.T) { + doc, id1, id2, _ := prepareDocument(t) + inv := invoicepb.InvoiceData{ + InvoiceNumber: "1234556", + Currency: "EUR", + GrossAmount: 1234, + SenderName: "john doe", + Comment: "Some comment", + } + + prefix, compact := "invoice", []byte{0, 1, 0, 0} + // add rules to id1 to update anything on the invoice + createTransitionRules(t, doc, id1, compact, coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_PREFIX) + + // id2 can only update comment on invoice and nothing else + createTransitionRules(t, doc, id2, append(compact, []byte{0, 0, 0, 21}...), coredocumentpb.FieldMatchType_FIELD_MATCH_TYPE_EXACT) + + inv2 := inv + inv2.GrossAmount = 12340 + + // check if id1 made the update + assert.NoError(t, testInvoiceChange(t, doc, id1, &inv, &inv2, prefix, compact)) + + // id2 should fail since it can only change comment + // errors should be 1 + err := testInvoiceChange(t, doc, id2, &inv, &inv2, prefix, compact) + assert.Error(t, err) + assert.Equal(t, 1, errors.Len(err)) + + inv2 = inv + inv2.Comment = "new comment" + + // check if id1 made the update + assert.NoError(t, testInvoiceChange(t, doc, id1, &inv, &inv2, prefix, compact)) + + // id2 update should go through since the update was to comment + assert.NoError(t, testInvoiceChange(t, doc, id2, &inv, &inv2, prefix, compact)) +} + +func TestWriteACLs_initTransitionRules(t *testing.T) { + cd, err := newCoreDocument() + assert.NoError(t, err) + cd.initTransitionRules(nil, nil) + assert.Nil(t, cd.Document.Roles) + assert.Nil(t, cd.Document.TransitionRules) + + collab := []identity.DID{testingidentity.GenerateRandomDID()} + cd.initTransitionRules(collab, nil) + assert.Len(t, cd.Document.TransitionRules, 2) + assert.Len(t, cd.Document.Roles, 1) + + cd.initTransitionRules(collab, nil) + assert.Len(t, cd.Document.TransitionRules, 2) + assert.Len(t, cd.Document.Roles, 1) +} diff --git a/ethereum/geth_client.go b/ethereum/geth_client.go index 6cc0c4513..927e1d1f9 100644 --- a/ethereum/geth_client.go +++ b/ethereum/geth_client.go @@ -203,11 +203,27 @@ func QueueEthTXStatusTask( txID transactions.TxID, txHash common.Hash, queuer queue.TaskQueuer) (res queue.TaskResult, err error) { - return queuer.EnqueueJobWithMaxTries(EthTXStatusTaskName, map[string]interface{}{ + return QueueEthTXStatusTaskWithValue(accountID, txID, txHash, queuer, nil) +} + +// QueueEthTXStatusTaskWithValue starts a new queuing transaction check task with a filtered value. +func QueueEthTXStatusTaskWithValue( + accountID identity.DID, + txID transactions.TxID, + txHash common.Hash, + queuer queue.TaskQueuer, + txValue *transactions.TXValue) (res queue.TaskResult, err error) { + params := map[string]interface{}{ transactions.TxIDParam: txID.String(), TransactionAccountParam: accountID.String(), TransactionTxHashParam: txHash.String(), - }) + } + if txValue != nil { + params[TransactionEventName] = txValue.Key + params[TransactionEventValueIdx] = txValue.KeyIdx + } + + return queuer.EnqueueJobWithMaxTries(EthTXStatusTaskName, params) } /** diff --git a/ethereum/geth_client_integration_test.go b/ethereum/geth_client_integration_test.go index 1b654d8c3..339eab60f 100644 --- a/ethereum/geth_client_integration_test.go +++ b/ethereum/geth_client_integration_test.go @@ -79,6 +79,7 @@ func TestMain(m *testing.M) { } func TestGetConnection_returnsSameConnection(t *testing.T) { + t.Parallel() howMany := 5 confChannel := make(chan ethereum.Client, howMany) for ix := 0; ix < howMany; ix++ { diff --git a/ethereum/transaction_status_task.go b/ethereum/transaction_status_task.go index 7bf6bc418..4a97e8c3a 100644 --- a/ethereum/transaction_status_task.go +++ b/ethereum/transaction_status_task.go @@ -4,6 +4,9 @@ import ( "context" "time" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/centrifuge/go-centrifuge/transactions/txv1" "github.com/centrifuge/go-centrifuge/errors" @@ -25,6 +28,13 @@ const ( // TransactionAccountParam contains the name of the account TransactionAccountParam string = "Account ID" + + // TransactionEventName contains the name of the event filtered + TransactionEventName string = "TxEventName" + + // TransactionEventValueIdx contains the index of the position of the event value + TransactionEventValueIdx string = "TxEventValueIdx" + // TransactionStatusSuccess contains the flag for a successful receipt.status TransactionStatusSuccess uint64 = 1 @@ -52,6 +62,10 @@ type TransactionStatusTask struct { //txHash is the id of an Ethereum transaction txHash string accountID identity.DID + + //event filter + eventName string + eventValueIdx int } // NewTransactionStatusTask returns a the struct for the task @@ -117,6 +131,23 @@ func (tst *TransactionStatusTask) ParseKwargs(kwargs map[string]interface{}) (er return errors.New("malformed kwarg [%s]", TransactionTxHashParam) } + // parse txEventName and index + txEventName, ok := kwargs[TransactionEventName] + if ok { + tst.eventName, ok = txEventName.(string) + if !ok { + return errors.New("malformed kwarg [%s]", TransactionEventName) + } + txEventValueIdx, ok := kwargs[TransactionEventValueIdx] + if !ok { + return errors.New("undefined kwarg " + TransactionEventValueIdx) + } + tst.eventValueIdx, err = GetInt(txEventValueIdx) + if err != nil { + return err + } + } + // override TimeoutParam if provided tdRaw, ok := kwargs[queue.TimeoutParam] if ok { @@ -130,6 +161,32 @@ func (tst *TransactionStatusTask) ParseKwargs(kwargs map[string]interface{}) (er return nil } +// GetInt converts key interface (float64) to int (used queueing only) +func GetInt(key interface{}) (int, error) { + f64, ok := key.(float64) + if !ok { + return 0, errors.New("Could not parse interface to float64") + } + return int(f64), nil +} + +// getEventsFromTransactionReceipt returns all events that are indexed +// note that events that are not indexed will not be parsed at the moment +func (tst *TransactionStatusTask) getEventValueFromTransactionReceipt(ctx context.Context, txHash string, event string, idxValue int) (value []byte, err error) { + receipt, err := tst.transactionReceipt(ctx, common.HexToHash(txHash)) + if err != nil { + return nil, err + } + for _, v := range receipt.Logs { + if (len(v.Topics) > 0) && v.Topics[0].Hex() == hexutil.Encode(crypto.Keccak256([]byte(event))) { + if idxValue < len(v.Topics) { + return v.Topics[idxValue+1].Bytes(), nil + } + } + } + return nil, errors.New("Event [%s] with value idx [%d] not found", event, idxValue) +} + func (tst *TransactionStatusTask) isTransactionSuccessful(ctx context.Context, txHash string) error { receipt, err := tst.transactionReceipt(ctx, common.HexToHash(txHash)) if err != nil { @@ -145,10 +202,11 @@ func (tst *TransactionStatusTask) isTransactionSuccessful(ctx context.Context, t // RunTask calls listens to events from geth related to MintingConfirmationTask#TokenID and records result. func (tst *TransactionStatusTask) RunTask() (resp interface{}, err error) { + var txValue *transactions.TXValue ctx, cancelF := tst.ethContextInitializer(tst.timeout) defer cancelF() defer func() { - err = tst.UpdateTransaction(tst.accountID, tst.TaskTypeName(), err) + err = tst.UpdateTransactionWithValue(tst.accountID, tst.TaskTypeName(), err, txValue) }() _, isPending, err := tst.transactionByHash(ctx, common.HexToHash(tst.txHash)) @@ -158,7 +216,6 @@ func (tst *TransactionStatusTask) RunTask() (resp interface{}, err error) { if err == ethereum.NotFound { err = gocelery.ErrTaskRetryable } - return nil, err } @@ -167,13 +224,21 @@ func (tst *TransactionStatusTask) RunTask() (resp interface{}, err error) { } err = tst.isTransactionSuccessful(ctx, tst.txHash) - if err == nil { - return nil, nil + if err != nil { + if err != ErrTransactionFailed { + err = gocelery.ErrTaskRetryable + } + return nil, err } - if err != ErrTransactionFailed { - return nil, gocelery.ErrTaskRetryable + if tst.eventName != "" { + v, err := tst.getEventValueFromTransactionReceipt(ctx, tst.txHash, tst.eventName, tst.eventValueIdx) + if err != nil { + return nil, err + } + log.Infof("Value [%x] found for Event [%s]\n", v, tst.eventName) + txValue = &transactions.TXValue{Key: tst.eventName, Value: v} } - return nil, err + return nil, nil } diff --git a/ethereum/transaction_status_task_integration_test.go b/ethereum/transaction_status_task_integration_test.go index 681d0afc3..5f9c733bb 100644 --- a/ethereum/transaction_status_task_integration_test.go +++ b/ethereum/transaction_status_task_integration_test.go @@ -42,6 +42,7 @@ func enqueueJob(t *testing.T, txHash string) (transactions.Manager, identity.DID } func TestTransactionStatusTask_successful(t *testing.T) { + t.Parallel() txManager, cid, tx, result := enqueueJob(t, "0x1") r := <-result @@ -52,6 +53,7 @@ func TestTransactionStatusTask_successful(t *testing.T) { } func TestTransactionStatusTask_failed(t *testing.T) { + t.Parallel() txManager, cid, tx, result := enqueueJob(t, "0x2") r := <-result diff --git a/ethereum/transaction_status_task_test.go b/ethereum/transaction_status_task_test.go index 9c071b1e3..93f30eed1 100644 --- a/ethereum/transaction_status_task_test.go +++ b/ethereum/transaction_status_task_test.go @@ -3,7 +3,15 @@ package ethereum import ( + "context" "testing" + "time" + + "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/mock" "github.com/centrifuge/go-centrifuge/testingutils/identity" @@ -35,8 +43,37 @@ func TestMintingConfirmationTask_ParseKwargs_success(t *testing.T) { } +func TestMintingConfirmationTask_ParseKwargsWithEvents_success(t *testing.T) { + task := TransactionStatusTask{} + txHash := "0xd18036d7c1fe109af377e8ce1d9096e69a5df0741fba7e4f3507f8e6aa573515" + txID := transactions.NewTxID().String() + cid := testingidentity.GenerateRandomDID() + eventName := "IdentityCreated(address)" + eventIdx := 0 + + kwargs := map[string]interface{}{ + transactions.TxIDParam: txID, + TransactionAccountParam: cid.String(), + TransactionTxHashParam: txHash, + TransactionEventName: eventName, + TransactionEventValueIdx: eventIdx, + } + + decoded, err := utils.SimulateJSONDecodeForGocelery(kwargs) + assert.Nil(t, err, "json decode should not thrown an error") + err = task.ParseKwargs(decoded) + assert.Nil(t, err, "parsing should be successful") + + assert.Equal(t, cid, task.accountID, "accountID should be parsed correctly") + assert.Equal(t, txID, task.TxID.String(), "txID should be parsed correctly") + assert.Equal(t, txHash, task.txHash, "txHash should be parsed correctly") + assert.Equal(t, eventName, task.eventName, "eventName should be parsed correctly") + assert.Equal(t, eventIdx, task.eventValueIdx, "eventValueIdx should be parsed correctly") +} + func TestMintingConfirmationTask_ParseKwargs_fail(t *testing.T) { task := TransactionStatusTask{} + eventName := "IdentityCreated(address)" tests := []map[string]interface{}{ { transactions.TxIDParam: transactions.NewTxID().String(), @@ -50,6 +87,25 @@ func TestMintingConfirmationTask_ParseKwargs_fail(t *testing.T) { transactions.TxIDParam: transactions.NewTxID().String(), TransactionTxHashParam: "0xd18036d7c1fe109af377e8ce1d9096e69a5df0741fba7e4f3507f8e6aa573515", }, + { + transactions.TxIDParam: transactions.NewTxID().String(), + TransactionAccountParam: testingidentity.GenerateRandomDID().String(), + TransactionTxHashParam: "0xd18036d7c1fe109af377e8ce1d9096e69a5df0741fba7e4f3507f8e6aa573515", + TransactionEventName: 0, + }, + { + transactions.TxIDParam: transactions.NewTxID().String(), + TransactionAccountParam: testingidentity.GenerateRandomDID().String(), + TransactionTxHashParam: "0xd18036d7c1fe109af377e8ce1d9096e69a5df0741fba7e4f3507f8e6aa573515", + TransactionEventName: eventName, + }, + { + transactions.TxIDParam: transactions.NewTxID().String(), + TransactionAccountParam: testingidentity.GenerateRandomDID().String(), + TransactionTxHashParam: "0xd18036d7c1fe109af377e8ce1d9096e69a5df0741fba7e4f3507f8e6aa573515", + TransactionEventName: eventName, + TransactionEventValueIdx: "wrong", + }, { //empty map @@ -66,3 +122,66 @@ func TestMintingConfirmationTask_ParseKwargs_fail(t *testing.T) { assert.Error(t, err, "test case %v: parsing should fail", i) } } + +func TestGetEventValueFromTransactionReceipt(t *testing.T) { + eventName := "IdentityCreated(address)" + eventNameHash := common.BytesToHash(crypto.Keccak256([]byte(eventName))) + eventValue := []byte{0, 1, 2, 3, 4} + wrongEvent := "WrongEvent(bytes)" + eventIdx := 0 + mockClient := &testingcommons.MockEthClient{} + + // Empty event list error + mockClient.On("TransactionReceipt", mock.Anything, common.HexToHash("0x1")).Return(&types.Receipt{Status: 1}, nil).Once() + ethTransTask := NewTransactionStatusTask(200*time.Millisecond, nil, nil, mockClient.TransactionReceipt, nil) + v, err := ethTransTask.getEventValueFromTransactionReceipt(context.Background(), "0x1", eventName, eventIdx) + assert.Error(t, err) + assert.Nil(t, v) + + // Logs missing topics error + receiptLog := &types.Log{} + mockClient.On("TransactionReceipt", mock.Anything, common.HexToHash("0x1")).Return(&types.Receipt{Status: 1, Logs: []*types.Log{receiptLog}}, nil).Once() + ethTransTask = NewTransactionStatusTask(200*time.Millisecond, nil, nil, mockClient.TransactionReceipt, nil) + v, err = ethTransTask.getEventValueFromTransactionReceipt(context.Background(), "0x1", eventName, eventIdx) + assert.Error(t, err) + assert.Nil(t, v) + + // wrong event filtered + receiptLog = &types.Log{ + Topics: []common.Hash{ + eventNameHash, + }, + } + mockClient.On("TransactionReceipt", mock.Anything, common.HexToHash("0x1")).Return(&types.Receipt{Status: 1, Logs: []*types.Log{receiptLog}}, nil).Once() + ethTransTask = NewTransactionStatusTask(200*time.Millisecond, nil, nil, mockClient.TransactionReceipt, nil) + v, err = ethTransTask.getEventValueFromTransactionReceipt(context.Background(), "0x1", wrongEvent, eventIdx) + assert.Error(t, err) + assert.Nil(t, v) + + // wrong event idx filtered + receiptLog = &types.Log{ + Topics: []common.Hash{ + eventNameHash, + common.BytesToHash(eventValue), + }, + } + mockClient.On("TransactionReceipt", mock.Anything, common.HexToHash("0x1")).Return(&types.Receipt{Status: 1, Logs: []*types.Log{receiptLog}}, nil).Once() + ethTransTask = NewTransactionStatusTask(200*time.Millisecond, nil, nil, mockClient.TransactionReceipt, nil) + v, err = ethTransTask.getEventValueFromTransactionReceipt(context.Background(), "0x1", eventName, 2) + assert.Error(t, err) + assert.Nil(t, v) + + // Success + receiptLog = &types.Log{ + Topics: []common.Hash{ + eventNameHash, + common.BytesToHash(eventValue), + }, + } + mockClient.On("TransactionReceipt", mock.Anything, common.HexToHash("0x1")).Return(&types.Receipt{Status: 1, Logs: []*types.Log{receiptLog}}, nil).Once() + ethTransTask = NewTransactionStatusTask(200*time.Millisecond, nil, nil, mockClient.TransactionReceipt, nil) + v, err = ethTransTask.getEventValueFromTransactionReceipt(context.Background(), "0x1", eventName, eventIdx) + assert.NoError(t, err) + assert.Equal(t, []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x3, 0x4}, v) + +} diff --git a/identity/did.go b/identity/did.go index 83ec21c4b..0e4b4cae6 100644 --- a/identity/did.go +++ b/identity/did.go @@ -6,7 +6,6 @@ import ( "math/big" "time" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/crypto/ed25519" "github.com/centrifuge/go-centrifuge/errors" @@ -181,9 +180,16 @@ func NewDIDFromBytes(bAddr []byte) DID { // return NewDID(common.BytesToAddress(addressByte)), nil //} +// IDTX abstracts transactions.TxID for identity package +type IDTX interface { + String() string + Bytes() []byte +} + // Factory is the interface for factory related interactions type Factory interface { CreateIdentity(ctx context.Context) (id *DID, err error) + IdentityExists(did *DID) (exists bool, err error) CalculateIdentityAddress(ctx context.Context) (*common.Address, error) } @@ -199,13 +205,10 @@ type ServiceDID interface { GetKey(did DID, key [32]byte) (*KeyResponse, error) // RawExecute calls the execute method on the identity contract - RawExecute(ctx context.Context, to common.Address, data []byte) error + RawExecute(ctx context.Context, to common.Address, data []byte) (txID IDTX, done chan bool, err error) // Execute creates the abi encoding an calls the execute method on the identity contract - Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) error - - // IsSignedWithPurpose verifies if a message is signed with one of the identities specific purpose keys - IsSignedWithPurpose(did DID, message [32]byte, signature []byte, purpose *big.Int) (bool, error) + Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) (txID IDTX, done chan bool, err error) // AddMultiPurposeKey adds a key with multiple purposes AddMultiPurposeKey(context context.Context, key [32]byte, purposes []*big.Int, keyType *big.Int) error @@ -220,10 +223,10 @@ type ServiceDID interface { Exists(ctx context.Context, did DID) error // ValidateKey checks if a given key is valid for the given centrifugeID. - ValidateKey(ctx context.Context, did DID, key []byte, purpose *big.Int) error + ValidateKey(ctx context.Context, did DID, key []byte, purpose *big.Int, at *time.Time) error // ValidateSignature checks if signature is valid for given identity - ValidateSignature(signature *coredocumentpb.Signature, message []byte) error + ValidateSignature(did DID, pubKey []byte, signature []byte, message []byte, timestamp time.Time) error // CurrentP2PKey retrieves the last P2P key stored in the identity CurrentP2PKey(did DID) (ret string, err error) @@ -233,14 +236,14 @@ type ServiceDID interface { GetClientsP2PURLs(dids []*DID) ([]string, error) // GetKeysByPurpose returns keys grouped by purpose from the identity contract. - GetKeysByPurpose(did DID, purpose *big.Int) ([][32]byte, error) + GetKeysByPurpose(did DID, purpose *big.Int) ([]KeyDID, error) } // KeyDID defines a single ERC725 identity key type KeyDID interface { GetKey() [32]byte GetPurpose() *big.Int - GetRevokedAt() *big.Int + GetRevokedAt() uint32 GetType() *big.Int } @@ -248,20 +251,20 @@ type KeyDID interface { type KeyResponse struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 } // Key holds the identity related details type key struct { Key [32]byte Purpose *big.Int - RevokedAt *big.Int + RevokedAt uint32 Type *big.Int } //NewKey returns a new key struct -func NewKey(pk [32]byte, purpose *big.Int, keyType *big.Int) KeyDID { - return &key{pk, purpose, big.NewInt(0), keyType} +func NewKey(pk [32]byte, purpose *big.Int, keyType *big.Int, revokedAt uint32) KeyDID { + return &key{pk, purpose, revokedAt, keyType} } // GetKey returns the public key @@ -275,7 +278,7 @@ func (idk *key) GetPurpose() *big.Int { } // GetRevokedAt returns the block at which the identity is revoked -func (idk *key) GetRevokedAt() *big.Int { +func (idk *key) GetRevokedAt() uint32 { return idk.RevokedAt } @@ -310,3 +313,13 @@ type Config interface { GetSigningKeyPair() (pub, priv string) GetEthereumContextWaitTimeout() time.Duration } + +// ValidateDIDBytes validates a centrifuge ID given as bytes +func ValidateDIDBytes(givenDID []byte, did DID) error { + calcdid := NewDIDFromBytes(givenDID) + if !did.Equal(calcdid) { + return errors.New("provided bytes doesn't match centID") + } + + return nil +} diff --git a/identity/ideth/execute_integration_test.go b/identity/ideth/execute_integration_test.go index ce731f1ca..4c2572e18 100644 --- a/identity/ideth/execute_integration_test.go +++ b/identity/ideth/execute_integration_test.go @@ -56,7 +56,7 @@ func TestExecute_successful(t *testing.T) { //add action key actionKey := utils.AddressTo32Bytes(common.HexToAddress(actionAddress)) - key := id.NewKey(actionKey, &(id.KeyPurposeAction.Value), utils.ByteSliceToBigInt([]byte{123})) + key := id.NewKey(actionKey, &(id.KeyPurposeAction.Value), utils.ByteSliceToBigInt([]byte{123}), 0) err = idSrv.AddKey(aCtx, key) assert.NoError(t, err) @@ -70,7 +70,9 @@ func TestExecute_successful(t *testing.T) { proofs := [][anchors.DocumentProofLength]byte{utils.RandomByte32()} // call execute - err = idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "commit", testAnchorIdPreimage.BigInt(), testRootHash, proofs) + _, done, err := idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "commit", testAnchorIdPreimage.BigInt(), testRootHash, proofs) + isDone := <-done + assert.True(t, isDone) assert.Nil(t, err, "Execute method calls should be successful") checkAnchor(t, testAnchorId, rootHash) @@ -90,9 +92,8 @@ func TestExecute_fail_falseMethodName(t *testing.T) { proofs := [][anchors.DocumentProofLength]byte{utils.RandomByte32()} - err := idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "fakeMethod", testAnchorId.BigInt(), testRootHash, proofs) + _, _, err := idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "fakeMethod", testAnchorId.BigInt(), testRootHash, proofs) assert.Error(t, err, "should throw an error because method is not existing in abi") - resetDefaultCentID() } @@ -106,8 +107,8 @@ func TestExecute_fail_MissingParam(t *testing.T) { rootHash := utils.RandomSlice(32) testRootHash, _ := anchors.ToDocumentRoot(rootHash) - err := idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "commit", testAnchorId.BigInt(), testRootHash) - assert.Error(t, err, "should throw an error because method is not existing in abi") + _, _, err := idSrv.Execute(aCtx, anchorAddress, anchors.AnchorContractABI, "commit", testAnchorId.BigInt(), testRootHash) + assert.Error(t, err, "should throw an error because wrong params as per abi") resetDefaultCentID() } diff --git a/identity/ideth/factory.go b/identity/ideth/factory.go index c13fc2f11..dc4500fcf 100644 --- a/identity/ideth/factory.go +++ b/identity/ideth/factory.go @@ -17,6 +17,8 @@ import ( var log = logging.Logger("identity") +const identityCreatedEventName = "IdentityCreated(address)" + type factory struct { factoryAddress common.Address factoryContract *FactoryContract @@ -54,7 +56,7 @@ func (s *factory) createIdentityTX(opts *bind.TransactOpts) func(accountID id.DI log.Infof("Sent off identity creation Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", ethTX.Hash(), ethTX.Nonce(), ethTX.CheckNonce()) log.Infof("Transfer pending: 0x%x\n", ethTX.Hash()) - res, err := ethereum.QueueEthTXStatusTask(accountID, txID, ethTX.Hash(), s.queue) + res, err := ethereum.QueueEthTXStatusTaskWithValue(accountID, txID, ethTX.Hash(), s.queue, &transactions.TXValue{Key: identityCreatedEventName, KeyIdx: 0}) if err != nil { errOut <- err return @@ -95,6 +97,15 @@ func isIdentityContract(identityAddress common.Address, client ethereum.Client) } +func (s *factory) IdentityExists(did *id.DID) (exists bool, err error) { + opts, _ := s.client.GetGethCallOpts(false) + valid, err := s.factoryContract.CreatedIdentity(opts, did.ToAddress()) + if err != nil { + return false, err + } + return valid, nil +} + func (s *factory) CreateIdentity(ctx context.Context) (did *id.DID, err error) { tc, err := contextutil.Account(ctx) if err != nil { @@ -107,12 +118,12 @@ func (s *factory) CreateIdentity(ctx context.Context) (did *id.DID, err error) { return nil, err } - identityAddress, err := s.CalculateIdentityAddress(ctx) + calcIdentityAddress, err := s.CalculateIdentityAddress(ctx) if err != nil { return nil, err } - createdDID := id.NewDID(*identityAddress) + createdDID := id.NewDID(*calcIdentityAddress) txID, done, err := s.txManager.ExecuteWithinTX(context.Background(), createdDID, transactions.NilTxID(), "Check TX for create identity status", s.createIdentityTX(opts)) if err != nil { @@ -123,13 +134,31 @@ func (s *factory) CreateIdentity(ctx context.Context) (did *id.DID, err error) { // non async task if !isDone { return nil, errors.New("Create Identity TX failed: txID:%s", txID.String()) + } + tx, err := s.txManager.GetTransaction(createdDID, txID) + if err != nil { + return nil, err + } + idCreated, ok := tx.Values[identityCreatedEventName] + if !ok { + return nil, errors.New("Couldn't find value for %s", identityCreatedEventName) } + createdAddr := common.BytesToAddress(idCreated.Value) + log.Infof("ID Created with address: %s", createdAddr.Hex()) - err = isIdentityContract(*identityAddress, s.client) + if calcIdentityAddress.Hex() != createdAddr.Hex() { + log.Infof("[Recovered] Found race condition creating identity, calculatedDID[%s] vs createdDID[%s]", calcIdentityAddress.Hex(), createdAddr.Hex()) + } + + createdDID = id.NewDID(createdAddr) + exists, err := s.IdentityExists(&createdDID) if err != nil { return nil, err } + if !exists { + return nil, errors.New("Identity %s not found in factory registry", createdDID.String()) + } return &createdDID, nil } diff --git a/identity/ideth/factory_integration_test.go b/identity/ideth/factory_integration_test.go index ad44af8ea..1da022fff 100644 --- a/identity/ideth/factory_integration_test.go +++ b/identity/ideth/factory_integration_test.go @@ -52,19 +52,13 @@ func TestMain(m *testing.M) { } func TestCreateIdentity_successful(t *testing.T) { - factory := ctx[identity.BootstrappedDIDFactory].(identity.Factory) - accountCtx := testingconfig.CreateAccountContext(t, cfg) - did, err := factory.CreateIdentity(accountCtx) assert.Nil(t, err, "create identity should be successful") client := ctx[ethereum.BootstrappedEthereumClient].(ethereum.Client) - contractCode, err := client.GetEthClient().CodeAt(context.Background(), did.ToAddress(), nil) assert.Nil(t, err, "should be successful to get the contract code") - assert.Equal(t, true, len(contractCode) > 3000, "current contract code should be around 3378 bytes") - } diff --git a/identity/ideth/identity_contract.go b/identity/ideth/identity_contract.go index fd54ad483..bd5a0b6b5 100644 --- a/identity/ideth/identity_contract.go +++ b/identity/ideth/identity_contract.go @@ -28,7 +28,7 @@ var ( ) // IdentityContractABI is the input ABI used to generate the binding from. -const IdentityContractABI = "[{\"constant\":true,\"inputs\":[{\"name\":\"keyHash\",\"type\":\"bytes32\"}],\"name\":\"getKey\",\"outputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"},{\"name\":\"revokedAt\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"},{\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"addMultiPurposeKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purpose\",\"type\":\"uint256\"},{\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"addKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"}],\"name\":\"revokeKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"addressToKey\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"stateMutability\":\"pure\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"purpose\",\"type\":\"uint256\"}],\"name\":\"getKeysByPurpose\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32[]\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purpose\",\"type\":\"uint256\"}],\"name\":\"keyHasPurpose\",\"outputs\":[{\"name\":\"found\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"managementAddress\",\"type\":\"address\"},{\"name\":\"keys\",\"type\":\"bytes32[]\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"key\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"purpose\",\"type\":\"uint256\"},{\"indexed\":true,\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"KeyAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"key\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"revokedAt\",\"type\":\"uint256\"},{\"indexed\":true,\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"KeyRevoked\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"execute\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"},{\"name\":\"result\",\"type\":\"bytes\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"message\",\"type\":\"bytes32\"},{\"name\":\"signature\",\"type\":\"bytes\"},{\"name\":\"purpose\",\"type\":\"uint256\"}],\"name\":\"isSignedWithPurpose\",\"outputs\":[{\"name\":\"valid\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" +const IdentityContractABI = "[{\"constant\":true,\"inputs\":[{\"name\":\"value\",\"type\":\"bytes32\"}],\"name\":\"getKey\",\"outputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"},{\"name\":\"revokedAt\",\"type\":\"uint32\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"},{\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"addMultiPurposeKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purpose\",\"type\":\"uint256\"},{\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"addKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"}],\"name\":\"revokeKey\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"addressToKey\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"stateMutability\":\"pure\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"purpose\",\"type\":\"uint256\"}],\"name\":\"getKeysByPurpose\",\"outputs\":[{\"name\":\"keysByPurpose\",\"type\":\"bytes32[]\"},{\"name\":\"keyTypes\",\"type\":\"uint256[]\"},{\"name\":\"keysRevokedAt\",\"type\":\"uint32[]\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"key\",\"type\":\"bytes32\"},{\"name\":\"purpose\",\"type\":\"uint256\"}],\"name\":\"keyHasPurpose\",\"outputs\":[{\"name\":\"found\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"managementAddress\",\"type\":\"address\"},{\"name\":\"keys\",\"type\":\"bytes32[]\"},{\"name\":\"purposes\",\"type\":\"uint256[]\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"key\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"purpose\",\"type\":\"uint256\"},{\"indexed\":true,\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"KeyAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"key\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"revokedAt\",\"type\":\"uint32\"},{\"indexed\":true,\"name\":\"keyType\",\"type\":\"uint256\"}],\"name\":\"KeyRevoked\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"execute\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"},{\"name\":\"result\",\"type\":\"bytes\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" // IdentityContract is an auto generated Go binding around an Ethereum contract. type IdentityContract struct { @@ -200,96 +200,84 @@ func (_IdentityContract *IdentityContractCallerSession) AddressToKey(addr common // GetKey is a free data retrieval call binding the contract method 0x12aaac70. // -// Solidity: function getKey(keyHash bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint256) -func (_IdentityContract *IdentityContractCaller) GetKey(opts *bind.CallOpts, keyHash [32]byte) (struct { +// Solidity: function getKey(value bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint32) +func (_IdentityContract *IdentityContractCaller) GetKey(opts *bind.CallOpts, value [32]byte) (struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 }, error) { ret := new(struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 }) out := ret - err := _IdentityContract.contract.Call(opts, out, "getKey", keyHash) + err := _IdentityContract.contract.Call(opts, out, "getKey", value) return *ret, err } // GetKey is a free data retrieval call binding the contract method 0x12aaac70. // -// Solidity: function getKey(keyHash bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint256) -func (_IdentityContract *IdentityContractSession) GetKey(keyHash [32]byte) (struct { +// Solidity: function getKey(value bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint32) +func (_IdentityContract *IdentityContractSession) GetKey(value [32]byte) (struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 }, error) { - return _IdentityContract.Contract.GetKey(&_IdentityContract.CallOpts, keyHash) + return _IdentityContract.Contract.GetKey(&_IdentityContract.CallOpts, value) } // GetKey is a free data retrieval call binding the contract method 0x12aaac70. // -// Solidity: function getKey(keyHash bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint256) -func (_IdentityContract *IdentityContractCallerSession) GetKey(keyHash [32]byte) (struct { +// Solidity: function getKey(value bytes32) constant returns(key bytes32, purposes uint256[], revokedAt uint32) +func (_IdentityContract *IdentityContractCallerSession) GetKey(value [32]byte) (struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 }, error) { - return _IdentityContract.Contract.GetKey(&_IdentityContract.CallOpts, keyHash) + return _IdentityContract.Contract.GetKey(&_IdentityContract.CallOpts, value) } // GetKeysByPurpose is a free data retrieval call binding the contract method 0x9010f726. // -// Solidity: function getKeysByPurpose(purpose uint256) constant returns(bytes32[]) -func (_IdentityContract *IdentityContractCaller) GetKeysByPurpose(opts *bind.CallOpts, purpose *big.Int) ([][32]byte, error) { - var ( - ret0 = new([][32]byte) - ) - out := ret0 +// Solidity: function getKeysByPurpose(purpose uint256) constant returns(keysByPurpose bytes32[], keyTypes uint256[], keysRevokedAt uint32[]) +func (_IdentityContract *IdentityContractCaller) GetKeysByPurpose(opts *bind.CallOpts, purpose *big.Int) (struct { + KeysByPurpose [][32]byte + KeyTypes []*big.Int + KeysRevokedAt []uint32 +}, error) { + ret := new(struct { + KeysByPurpose [][32]byte + KeyTypes []*big.Int + KeysRevokedAt []uint32 + }) + out := ret err := _IdentityContract.contract.Call(opts, out, "getKeysByPurpose", purpose) - return *ret0, err + return *ret, err } // GetKeysByPurpose is a free data retrieval call binding the contract method 0x9010f726. // -// Solidity: function getKeysByPurpose(purpose uint256) constant returns(bytes32[]) -func (_IdentityContract *IdentityContractSession) GetKeysByPurpose(purpose *big.Int) ([][32]byte, error) { +// Solidity: function getKeysByPurpose(purpose uint256) constant returns(keysByPurpose bytes32[], keyTypes uint256[], keysRevokedAt uint32[]) +func (_IdentityContract *IdentityContractSession) GetKeysByPurpose(purpose *big.Int) (struct { + KeysByPurpose [][32]byte + KeyTypes []*big.Int + KeysRevokedAt []uint32 +}, error) { return _IdentityContract.Contract.GetKeysByPurpose(&_IdentityContract.CallOpts, purpose) } // GetKeysByPurpose is a free data retrieval call binding the contract method 0x9010f726. // -// Solidity: function getKeysByPurpose(purpose uint256) constant returns(bytes32[]) -func (_IdentityContract *IdentityContractCallerSession) GetKeysByPurpose(purpose *big.Int) ([][32]byte, error) { +// Solidity: function getKeysByPurpose(purpose uint256) constant returns(keysByPurpose bytes32[], keyTypes uint256[], keysRevokedAt uint32[]) +func (_IdentityContract *IdentityContractCallerSession) GetKeysByPurpose(purpose *big.Int) (struct { + KeysByPurpose [][32]byte + KeyTypes []*big.Int + KeysRevokedAt []uint32 +}, error) { return _IdentityContract.Contract.GetKeysByPurpose(&_IdentityContract.CallOpts, purpose) } -// IsSignedWithPurpose is a free data retrieval call binding the contract method 0x8699fd2b. -// -// Solidity: function isSignedWithPurpose(message bytes32, signature bytes, purpose uint256) constant returns(valid bool) -func (_IdentityContract *IdentityContractCaller) IsSignedWithPurpose(opts *bind.CallOpts, message [32]byte, signature []byte, purpose *big.Int) (bool, error) { - var ( - ret0 = new(bool) - ) - out := ret0 - err := _IdentityContract.contract.Call(opts, out, "isSignedWithPurpose", message, signature, purpose) - return *ret0, err -} - -// IsSignedWithPurpose is a free data retrieval call binding the contract method 0x8699fd2b. -// -// Solidity: function isSignedWithPurpose(message bytes32, signature bytes, purpose uint256) constant returns(valid bool) -func (_IdentityContract *IdentityContractSession) IsSignedWithPurpose(message [32]byte, signature []byte, purpose *big.Int) (bool, error) { - return _IdentityContract.Contract.IsSignedWithPurpose(&_IdentityContract.CallOpts, message, signature, purpose) -} - -// IsSignedWithPurpose is a free data retrieval call binding the contract method 0x8699fd2b. -// -// Solidity: function isSignedWithPurpose(message bytes32, signature bytes, purpose uint256) constant returns(valid bool) -func (_IdentityContract *IdentityContractCallerSession) IsSignedWithPurpose(message [32]byte, signature []byte, purpose *big.Int) (bool, error) { - return _IdentityContract.Contract.IsSignedWithPurpose(&_IdentityContract.CallOpts, message, signature, purpose) -} - // KeyHasPurpose is a free data retrieval call binding the contract method 0xd202158d. // // Solidity: function keyHasPurpose(key bytes32, purpose uint256) constant returns(found bool) @@ -620,15 +608,15 @@ func (it *IdentityContractKeyRevokedIterator) Close() error { // IdentityContractKeyRevoked represents a KeyRevoked event raised by the IdentityContract contract. type IdentityContractKeyRevoked struct { Key [32]byte - RevokedAt *big.Int + RevokedAt uint32 KeyType *big.Int Raw types.Log // Blockchain specific contextual infos } -// FilterKeyRevoked is a free log retrieval operation binding the contract event 0x8004a857c5cbc7c7c693a7c6a2852c373b5d03f882b57a8ee22dd2d4492331b1. +// FilterKeyRevoked is a free log retrieval operation binding the contract event 0x62db979b46b61a2c8ec127201e75b82b7a2dc57beb69834882857b7e9823d2fc. // -// Solidity: e KeyRevoked(key indexed bytes32, revokedAt indexed uint256, keyType indexed uint256) -func (_IdentityContract *IdentityContractFilterer) FilterKeyRevoked(opts *bind.FilterOpts, key [][32]byte, revokedAt []*big.Int, keyType []*big.Int) (*IdentityContractKeyRevokedIterator, error) { +// Solidity: e KeyRevoked(key indexed bytes32, revokedAt indexed uint32, keyType indexed uint256) +func (_IdentityContract *IdentityContractFilterer) FilterKeyRevoked(opts *bind.FilterOpts, key [][32]byte, revokedAt []uint32, keyType []*big.Int) (*IdentityContractKeyRevokedIterator, error) { var keyRule []interface{} for _, keyItem := range key { @@ -650,10 +638,10 @@ func (_IdentityContract *IdentityContractFilterer) FilterKeyRevoked(opts *bind.F return &IdentityContractKeyRevokedIterator{contract: _IdentityContract.contract, event: "KeyRevoked", logs: logs, sub: sub}, nil } -// WatchKeyRevoked is a free log subscription operation binding the contract event 0x8004a857c5cbc7c7c693a7c6a2852c373b5d03f882b57a8ee22dd2d4492331b1. +// WatchKeyRevoked is a free log subscription operation binding the contract event 0x62db979b46b61a2c8ec127201e75b82b7a2dc57beb69834882857b7e9823d2fc. // -// Solidity: e KeyRevoked(key indexed bytes32, revokedAt indexed uint256, keyType indexed uint256) -func (_IdentityContract *IdentityContractFilterer) WatchKeyRevoked(opts *bind.WatchOpts, sink chan<- *IdentityContractKeyRevoked, key [][32]byte, revokedAt []*big.Int, keyType []*big.Int) (event.Subscription, error) { +// Solidity: e KeyRevoked(key indexed bytes32, revokedAt indexed uint32, keyType indexed uint256) +func (_IdentityContract *IdentityContractFilterer) WatchKeyRevoked(opts *bind.WatchOpts, sink chan<- *IdentityContractKeyRevoked, key [][32]byte, revokedAt []uint32, keyType []*big.Int) (event.Subscription, error) { var keyRule []interface{} for _, keyItem := range key { diff --git a/identity/ideth/service.go b/identity/ideth/service.go index 6e439458a..06322ad62 100644 --- a/identity/ideth/service.go +++ b/identity/ideth/service.go @@ -5,8 +5,8 @@ import ( "fmt" "math/big" "strings" + "time" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/crypto" @@ -29,12 +29,14 @@ type contract interface { GetKey(opts *bind.CallOpts, _key [32]byte) (struct { Key [32]byte Purposes []*big.Int - RevokedAt *big.Int + RevokedAt uint32 }, error) - IsSignedWithPurpose(opts *bind.CallOpts, message [32]byte, _signature []byte, _purpose *big.Int) (bool, error) - - GetKeysByPurpose(opts *bind.CallOpts, purpose *big.Int) ([][32]byte, error) + GetKeysByPurpose(opts *bind.CallOpts, purpose *big.Int) (struct { + KeysByPurpose [][32]byte + KeyTypes []*big.Int + KeysRevokedAt []uint32 + }, error) // Ethereum Transactions AddKey(opts *bind.TransactOpts, _key [32]byte, _purpose *big.Int, _keyType *big.Int) (*types.Transaction, error) @@ -230,68 +232,56 @@ func (i service) GetKey(did id.DID, key [32]byte) (*id.KeyResponse, error) { } -// IsSignedWithPurpose verifies if a message is signed with one of the identities specific purpose keys -func (i service) IsSignedWithPurpose(did id.DID, message [32]byte, signature []byte, purpose *big.Int) (bool, error) { - contract, opts, _, err := i.prepareCall(did) - if err != nil { - return false, err - } - - return contract.IsSignedWithPurpose(opts, message, signature, purpose) - -} - // RawExecute calls the execute method on the identity contract -func (i service) RawExecute(ctx context.Context, to common.Address, data []byte) error { +// TODO once we clean up transaction to not use higher level deps we can change back the return to be transactions.txID +func (i service) RawExecute(ctx context.Context, to common.Address, data []byte) (txID id.IDTX, done chan bool, err error) { + utxID := contextutil.TX(ctx) DID, err := NewDIDFromContext(ctx) if err != nil { - return err + return transactions.NilTxID(), nil, err } contract, opts, err := i.prepareTransaction(ctx, DID) if err != nil { - return err + return transactions.NilTxID(), nil, err } // default: no ether should be send value := big.NewInt(0) - - txID, done, err := i.txManager.ExecuteWithinTX(context.Background(), DID, transactions.NilTxID(), "Check TX for execute", i.ethereumTX(opts, contract.Execute, to, value, data)) - if err != nil { - return err - } - - isDone := <-done - // non async task - if !isDone { - return errors.New("raw execute TX failed: txID:%s", txID.String()) - - } - return nil - + return i.txManager.ExecuteWithinTX(context.Background(), DID, utxID, "Check TX for execute", i.ethereumTX(opts, contract.Execute, to, value, data)) } // Execute creates the abi encoding an calls the execute method on the identity contract -func (i service) Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) error { - abi, err := abi.JSON(strings.NewReader(contractAbi)) +// TODO once we clean up transaction to not use higher level deps we can change back the return to be transactions.txID +func (i service) Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) (txID id.IDTX, done chan bool, err error) { + abiObj, err := abi.JSON(strings.NewReader(contractAbi)) if err != nil { - return err + return transactions.NilTxID(), nil, err } // Pack encodes the parameters and additionally checks if the method and arguments are defined correctly - data, err := abi.Pack(methodName, args...) + data, err := abiObj.Pack(methodName, args...) if err != nil { - return err + return transactions.NilTxID(), nil, err } return i.RawExecute(ctx, to, data) } -func (i service) GetKeysByPurpose(did id.DID, purpose *big.Int) ([][32]byte, error) { +func (i service) GetKeysByPurpose(did id.DID, purpose *big.Int) ([]id.KeyDID, error) { contract, opts, _, err := i.prepareCall(did) if err != nil { return nil, err } - return contract.GetKeysByPurpose(opts, purpose) + keyStruct, err := contract.GetKeysByPurpose(opts, purpose) + if err != nil { + return nil, err + } + + var keyResp []id.KeyDID + for i, k := range keyStruct.KeysByPurpose { + keyResp = append(keyResp, id.NewKey(k, purpose, keyStruct.KeyTypes[i], keyStruct.KeysRevokedAt[i])) + } + return keyResp, nil } @@ -303,12 +293,12 @@ func (i service) CurrentP2PKey(did id.DID) (ret string, err error) { } lastKey := keys[len(keys)-1] - key, err := i.GetKey(did, lastKey) + key, err := i.GetKey(did, lastKey.GetKey()) if err != nil { return "", err } - if key.RevokedAt.Cmp(big.NewInt(0)) != 0 { + if key.RevokedAt != 0 { return "", errors.New("current p2p key has been revoked") } @@ -336,7 +326,7 @@ func (i service) Exists(ctx context.Context, did id.DID) error { } // ValidateKey checks if a given key is valid for the given centrifugeID. -func (i service) ValidateKey(ctx context.Context, did id.DID, key []byte, purpose *big.Int) error { +func (i service) ValidateKey(ctx context.Context, did id.DID, key []byte, purpose *big.Int, validateAt *time.Time) error { contract, opts, _, err := i.prepareCall(did) if err != nil { return err @@ -347,12 +337,29 @@ func (i service) ValidateKey(ctx context.Context, did id.DID, key []byte, purpos return err } - keys, err := contract.GetKey(opts, key32) + ethKey, err := contract.GetKey(opts, key32) if err != nil { return err } - for _, p := range keys.Purposes { + // if revoked + if ethKey.RevokedAt > 0 { + // if a specific time for validation is provided then we validate if a revoked key was revoked before the provided time + if validateAt != nil { + revokedAtBlock, err := i.client.GetEthClient().BlockByNumber(ctx, big.NewInt(int64(ethKey.RevokedAt))) + if err != nil { + return err + } + + if big.NewInt(validateAt.Unix()).Cmp(revokedAtBlock.Time()) > 0 { + return errors.New("the given key [%x] for purpose [%s] has been revoked before provided time %s", key, purpose.String(), validateAt.String()) + } + } else { + return errors.New("the given key [%x] for purpose [%s] has been revoked and not valid anymore", key, purpose.String()) + } + } + + for _, p := range ethKey.Purposes { if p.Cmp(purpose) == 0 { return nil } @@ -386,7 +393,7 @@ func convertAccountKeysToKeyDID(accKeys map[string]config.IDKey) (map[string]id. return nil, err } v := id.GetPurposeByName(k).Value - keys[k] = id.NewKey(pk32, &v, big.NewInt(id.KeyTypeECDSA)) + keys[k] = id.NewKey(pk32, &v, big.NewInt(id.KeyTypeECDSA), 0) } return keys, nil } @@ -427,33 +434,20 @@ func (i service) AddKeysForAccount(acc config.Account) error { } // ValidateSignature validates a signature on a message based on identity data -func (i service) ValidateSignature(signature *coredocumentpb.Signature, message []byte) error { - centID := id.NewDIDFromBytes(signature.EntityId) - - err := i.ValidateKey(context.Background(), centID, signature.PublicKey, &(id.KeyPurposeSigning.Value)) +func (i service) ValidateSignature(did id.DID, pubKey []byte, signature []byte, message []byte, timestamp time.Time) error { + err := i.ValidateKey(context.Background(), did, pubKey, &(id.KeyPurposeSigning.Value), ×tamp) if err != nil { return err } - if !crypto.VerifyMessage(signature.PublicKey, message, signature.Signature, crypto.CurveSecp256K1) { + if !crypto.VerifyMessage(pubKey, message, signature, crypto.CurveSecp256K1) { return errors.New("error when validating signature") } return nil } -// ValidateCentrifugeIDBytes validates a centrifuge ID given as bytes -func ValidateCentrifugeIDBytes(givenDID []byte, DID id.DID) error { - calcCentID := id.NewDIDFromBytes(givenDID) - if !DID.Equal(calcCentID) { - return errors.New("provided bytes doesn't match centID") - } - - return nil -} - // NewDIDFromContext returns DID from context.Account -// TODO remove this function to identity/did.go as soon as IDConfig is removed otherwise there is a cyclic dep func NewDIDFromContext(ctx context.Context) (id.DID, error) { tc, err := contextutil.Account(ctx) if err != nil { diff --git a/identity/ideth/service_integration_test.go b/identity/ideth/service_integration_test.go index 9a71bf3c5..101cf580a 100644 --- a/identity/ideth/service_integration_test.go +++ b/identity/ideth/service_integration_test.go @@ -7,15 +7,13 @@ import ( "fmt" "math/big" "testing" + "time" "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/crypto/ed25519" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/crypto/secp256k1" - "github.com/ethereum/go-ethereum/common" - "github.com/centrifuge/go-centrifuge/bootstrap" id "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/queue" @@ -29,7 +27,7 @@ import ( ) func getTestKey() id.KeyDID { - return id.NewKey(utils.RandomByte32(), utils.ByteSliceToBigInt([]byte{123}), utils.ByteSliceToBigInt([]byte{123})) + return id.NewKey(utils.RandomByte32(), utils.ByteSliceToBigInt([]byte{123}), utils.ByteSliceToBigInt([]byte{123}), 0) } func initIdentity() id.ServiceDID { @@ -100,53 +98,6 @@ func TestServiceAddKey_fail(t *testing.T) { } -func TestService_IsSignedWithPurpose(t *testing.T) { - // create keys - pk, sk, err := secp256k1.GenerateSigningKeyPair() - address := common.HexToAddress(secp256k1.GetAddress(pk)) - address32Bytes := utils.AddressTo32Bytes(address) - assert.Nil(t, err, "should convert a address to 32 bytes") - - // purpose - purpose := utils.ByteSliceToBigInt([]byte{123}) - assert.Nil(t, err, "should generate signing key pair") - - // deploy identity and add key with purpose - did := deployIdentityContract(t) - aCtx := getTestDIDContext(t, *did) - idSrv := initIdentity() - key := id.NewKey(address32Bytes, purpose, utils.ByteSliceToBigInt([]byte{123})) - - err = idSrv.AddKey(aCtx, key) - assert.Nil(t, err, "add key should be successful") - - // sign a msg with keypair - msg := utils.RandomByte32() - signature, err := secp256k1.SignEthereum(msg[:], sk) - assert.Nil(t, err, "should sign a message") - - //correct signature and purpose - signed, err := idSrv.IsSignedWithPurpose(*did, msg, signature, purpose) - assert.Nil(t, err, "sign verify should not throw an error") - assert.True(t, signed, "signature should be correct") - - //false purpose - falsePurpose := utils.ByteSliceToBigInt([]byte{42}) - signed, err = idSrv.IsSignedWithPurpose(*did, msg, signature, falsePurpose) - assert.Nil(t, err, "sign verify should not throw an error") - assert.False(t, signed, "signature should be false (wrong purpose)") - - //false keypair - _, sk2, _ := secp256k1.GenerateSigningKeyPair() - signature, err = secp256k1.SignEthereum(msg[:], sk2) - assert.Nil(t, err, "should sign a message") - signed, err = idSrv.IsSignedWithPurpose(*did, msg, signature, purpose) - assert.Nil(t, err, "sign verify should not throw an error") - assert.False(t, signed, "signature should be wrong key pair") - resetDefaultCentID() - -} - func TestService_AddMultiPurposeKey(t *testing.T) { did := deployIdentityContract(t) aCtx := getTestDIDContext(t, *did) @@ -178,14 +129,15 @@ func TestService_RevokeKey(t *testing.T) { addKey(aCtx, t, *did, idSrv, testKey) response, err := idSrv.GetKey(*did, testKey.GetKey()) - assert.Equal(t, utils.ByteSliceToBigInt([]byte{0}), response.RevokedAt, "key should be not revoked") + assert.Equal(t, uint32(0), response.RevokedAt, "key should be not revoked") - idSrv.RevokeKey(aCtx, testKey.GetKey()) + err = idSrv.RevokeKey(aCtx, testKey.GetKey()) + assert.NoError(t, err) //check if key is revoked response, err = idSrv.GetKey(*did, testKey.GetKey()) assert.Nil(t, err, "get Key should be successful") - assert.NotEqual(t, utils.ByteSliceToBigInt([]byte{0}), response.RevokedAt, "key should be revoked") + assert.NotEqual(t, uint32(0), response.RevokedAt, "key should be revoked") resetDefaultCentID() } @@ -218,16 +170,49 @@ func TestValidateKey(t *testing.T) { var purpose *big.Int purpose = big.NewInt(123) // test purpose - err := idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose) + err := idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose, nil) assert.Nil(t, err, "key with purpose should exist") purpose = big.NewInt(1) //false purpose - err = idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose) + err = idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose, nil) assert.Error(t, err, "key with purpose should not exist") resetDefaultCentID() } +func TestValidateKey_revoked(t *testing.T) { + did := deployIdentityContract(t) + aCtx := getTestDIDContext(t, *did) + idSrv := initIdentity() + + testKey := getTestKey() + addKey(aCtx, t, *did, idSrv, testKey) + + err := idSrv.RevokeKey(aCtx, testKey.GetKey()) + assert.NoError(t, err) + + key32 := testKey.GetKey() + + var purpose *big.Int + purpose = big.NewInt(123) // test purpose + + err = idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose, nil) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "for purpose [123] has been revoked and not valid anymore") + } + + beforeRevocation := time.Now().Add(-20 * time.Second) + err = idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose, &beforeRevocation) + assert.NoError(t, err) + + afterRevocation := time.Now() + err = idSrv.ValidateKey(aCtx, *did, utils.Byte32ToSlice(key32), purpose, &afterRevocation) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "for purpose [123] has been revoked before provided time") + } + resetDefaultCentID() +} + func addP2PKeyTestGetClientP2PURL(t *testing.T) (*id.DID, string) { did := deployIdentityContract(t) aCtx := getTestDIDContext(t, *did) @@ -235,7 +220,7 @@ func addP2PKeyTestGetClientP2PURL(t *testing.T) (*id.DID, string) { p2pKey := utils.RandomByte32() - testKey := id.NewKey(p2pKey, &(identity.KeyPurposeP2PDiscovery.Value), utils.ByteSliceToBigInt([]byte{123})) + testKey := id.NewKey(p2pKey, &(identity.KeyPurposeP2PDiscovery.Value), utils.ByteSliceToBigInt([]byte{123}), 0) addKey(aCtx, t, *did, idSrv, testKey) url, err := idSrv.GetClientP2PURL(*did) diff --git a/nft/ethereum_payment_obligation.go b/nft/ethereum_payment_obligation.go index c319f635e..8450f8223 100644 --- a/nft/ethereum_payment_obligation.go +++ b/nft/ethereum_payment_obligation.go @@ -151,19 +151,14 @@ func (s *ethereumPaymentObligation) MintNFT(ctx context.Context, req MintNFTRequ func (s *ethereumPaymentObligation) minter(ctx context.Context, tokenID TokenID, model documents.Model, req MintNFTRequest) func(accountID identity.DID, txID transactions.TxID, txMan transactions.Manager, errOut chan<- error) { return func(accountID identity.DID, txID transactions.TxID, txMan transactions.Manager, errOut chan<- error) { - tc, err := contextutil.Account(ctx) + err := model.AddNFT(req.GrantNFTReadAccess, req.RegistryAddress, tokenID[:]) if err != nil { errOut <- err return } - err = model.AddNFT(req.GrantNFTReadAccess, req.RegistryAddress, tokenID[:]) - if err != nil { - errOut <- err - return - } - - _, _, done, err := s.docSrv.Update(contextutil.WithTX(ctx, txID), model) + txctx := contextutil.WithTX(ctx, txID) + _, _, done, err := s.docSrv.Update(txctx, model) if err != nil { errOut <- err return @@ -176,36 +171,21 @@ func (s *ethereumPaymentObligation) minter(ctx context.Context, tokenID TokenID, return } - requestData, err := s.prepareMintRequest(ctx, tokenID, accountID, req) + requestData, err := s.prepareMintRequest(txctx, tokenID, accountID, req) if err != nil { errOut <- errors.New("failed to prepare mint request: %v", err) return } - opts, err := s.ethClient.GetTxOpts(tc.GetEthereumDefaultAccountName()) - if err != nil { - errOut <- err - return - } - - contract, err := s.bindContract(req.RegistryAddress, s.ethClient) - if err != nil { - errOut <- err - return - } - // to common.Address, tokenId *big.Int, tokenURI string, anchorId *big.Int, properties [][]byte, values [][]byte, salts [][32]byte, proofs [][][32]byte - ethTX, err := s.ethClient.SubmitTransactionWithRetries(contract.Mint, opts, requestData.To, requestData.TokenID, - requestData.TokenURI, requestData.AnchorID, requestData.Props, requestData.Values, - requestData.Salts, requestData.Proofs) + utxID, done, err := s.identityService.Execute(ctx, req.RegistryAddress, EthereumPaymentObligationContractABI, "mint", requestData.To, requestData.TokenID, + requestData.TokenURI, requestData.AnchorID, requestData.Props, requestData.Values, requestData.Salts, requestData.Proofs) if err != nil { errOut <- err return } - - log.Infof("Sent off ethTX to mint [tokenID: %s, anchor: %x, nextAnchor: %s, registry: %s] to payment obligation contract. Ethereum transaction hash [%s] and Nonce [%d] and Check [%v]", - requestData.TokenID, requestData.AnchorID, hexutil.Encode(requestData.NextAnchorID.Bytes()), requestData.To.String(), ethTX.Hash().String(), ethTX.Nonce(), ethTX.CheckNonce()) - log.Infof("Transfer pending: %s\n", ethTX.Hash().String()) + log.Infof("Sent off ethTX to mint [tokenID: %s, anchor: %x, nextAnchor: %s, registry: %s] to payment obligation contract.", + requestData.TokenID, requestData.AnchorID, hexutil.Encode(requestData.NextAnchorID.Bytes()), requestData.To.String()) log.Debugf("To: %s", requestData.To.String()) log.Debugf("TokenID: %s", hexutil.Encode(requestData.TokenID.Bytes())) @@ -217,18 +197,17 @@ func (s *ethereumPaymentObligation) minter(ctx context.Context, tokenID TokenID, log.Debugf("Salts: %s", byte32SlicetoString(requestData.Salts)) log.Debugf("Proofs: %s", byteByte32SlicetoString(requestData.Proofs)) - res, err := ethereum.QueueEthTXStatusTask(accountID, txID, ethTX.Hash(), s.queue) - if err != nil { - errOut <- err + isDone = <-done + if !isDone { + // some problem occurred in a child task + errOut <- errors.New("mint nft failed for document %s and transaction %s", hexutil.Encode(req.DocumentID), utxID) return } - _, err = res.Get(txMan.GetDefaultTaskTimeout()) - if err != nil { - errOut <- err - return - } + log.Infof("Document %s minted successfully within transaction %s", hexutil.Encode(req.DocumentID), utxID) + errOut <- nil + return } } @@ -323,6 +302,10 @@ func convertToProofData(proofspb []*proofspb.Proof) (*proofData, error) { } props[i] = p.GetCompactName() values[i] = p.Value + // Scenario where it is a hashed field we copy the Hash value into the property value + if len(p.Value) == 0 && len(p.Salt) == 0 { + values[i] = p.Hash + } salts[i] = salt32 proofs[i] = property } diff --git a/nft/ethereum_payment_obligation_test.go b/nft/ethereum_payment_obligation_test.go index 7d711aa0d..3d5184238 100644 --- a/nft/ethereum_payment_obligation_test.go +++ b/nft/ethereum_payment_obligation_test.go @@ -153,7 +153,7 @@ func TestPaymentObligationService(t *testing.T) { { "happypath", func() (testingdocuments.MockService, *MockPaymentObligation, testingcommons.MockIdentityService, testingcommons.MockEthClient, testingconfig.MockConfig, *testingutils.MockQueue, *testingtx.MockTxManager) { - cd, err := documents.NewCoreDocumentWithCollaborators(nil) + cd, err := documents.NewCoreDocumentWithCollaborators(nil, nil) assert.NoError(t, err) cd.Document.DocumentRoot = utils.RandomSlice(32) proof := getDummyProof(&cd.Document) diff --git a/nft/payment_obligation_integration_test.go b/nft/payment_obligation_integration_test.go index 38e4fed43..391c78f28 100644 --- a/nft/payment_obligation_integration_test.go +++ b/nft/payment_obligation_integration_test.go @@ -4,10 +4,15 @@ package nft_test import ( "context" + "fmt" "os" "testing" "time" + "github.com/centrifuge/go-centrifuge/utils" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/go-centrifuge/bootstrap" cc "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/testingbootstrap" @@ -24,7 +29,6 @@ import ( "github.com/centrifuge/go-centrifuge/transactions" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" - "github.com/golang/protobuf/ptypes/timestamp" "github.com/stretchr/testify/assert" ) @@ -75,6 +79,8 @@ func prepareForNFTMinting(t *testing.T) (context.Context, []byte, common.Address assert.NoError(t, err) invSrv := service.(invoice.Service) dueDate := time.Now().Add(4 * 24 * time.Hour) + tm, err := utils.ToTimestamp(dueDate) + assert.NoError(t, err) model, err := invSrv.DeriveFromCreatePayload(ctx, &invoicepb.InvoiceCreatePayload{ Collaborators: []string{}, Data: &invoicepb.InvoiceData{ @@ -84,7 +90,7 @@ func prepareForNFTMinting(t *testing.T) (context.Context, []byte, common.Address GrossAmount: 123, NetAmount: 123, Currency: "EUR", - DueDate: ×tamp.Timestamp{Seconds: dueDate.Unix()}, + DueDate: tm, }, }) assert.NoError(t, err, "should not error out when creating invoice model") @@ -124,11 +130,20 @@ func TestPaymentObligationService_mint_grant_read_access(t *testing.T) { ctx, id, registry, depositAddr, invSrv, cid := prepareForNFTMinting(t) regAddr := registry.String() log.Info(regAddr) + acc, err := contextutil.Account(ctx) + assert.NoError(t, err) + accDIDBytes, err := acc.GetIdentityID() + assert.NoError(t, err) + keys, err := acc.GetKeys() + assert.NoError(t, err) + signerId := hexutil.Encode(append(accDIDBytes, keys[identity.KeyPurposeSigning.Name].PublicKey...)) + signingRoot := fmt.Sprintf("%s.%s", documents.DRTreePrefix, documents.SigningRootField) + signatureSender := fmt.Sprintf("%s.signatures[%s].signature", documents.SignaturesTreePrefix, signerId) req := nft.MintNFTRequest{ DocumentID: id, RegistryAddress: registry, DepositAddress: common.HexToAddress(depositAddr), - ProofFields: []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "invoice.sender", "invoice.invoice_status", documents.CDTreePrefix + ".next_version"}, + ProofFields: []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "invoice.sender", "invoice.invoice_status", signingRoot, signatureSender, documents.CDTreePrefix + ".next_version"}, GrantNFTReadAccess: true, SubmitNFTReadAccessProof: true, SubmitTokenProof: true, @@ -138,9 +153,9 @@ func TestPaymentObligationService_mint_grant_read_access(t *testing.T) { assert.NoError(t, err) cd, err := doc.PackCoreDocument() assert.NoError(t, err) - assert.Len(t, cd.Roles, 2) - assert.Len(t, cd.Roles[1].Nfts, 1) - newNFT := cd.Roles[1].Nfts[0] + assert.Len(t, cd.Roles, 3) + assert.Len(t, cd.Roles[2].Nfts, 1) + newNFT := cd.Roles[2].Nfts[0] enft, err := documents.ConstructNFT(registry, tokenID.BigInt().Bytes()) assert.NoError(t, err) assert.Equal(t, enft, newNFT) @@ -175,11 +190,20 @@ func TestEthereumPaymentObligation_MintNFT_no_grant_access(t *testing.T) { func mintNFTWithProofs(t *testing.T, grantAccess, tokenProof, readAccessProof bool) { ctx, id, registry, depositAddr, invSrv, cid := prepareForNFTMinting(t) + acc, err := contextutil.Account(ctx) + assert.NoError(t, err) + accDIDBytes, err := acc.GetIdentityID() + assert.NoError(t, err) + keys, err := acc.GetKeys() + assert.NoError(t, err) + signerId := hexutil.Encode(append(accDIDBytes, keys[identity.KeyPurposeSigning.Name].PublicKey...)) + signingRoot := fmt.Sprintf("%s.%s", documents.DRTreePrefix, documents.SigningRootField) + signatureSender := fmt.Sprintf("%s.signatures[%s].signature", documents.SignaturesTreePrefix, signerId) req := nft.MintNFTRequest{ DocumentID: id, RegistryAddress: registry, DepositAddress: common.HexToAddress(depositAddr), - ProofFields: []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "invoice.sender", "invoice.invoice_status", documents.CDTreePrefix + ".next_version"}, + ProofFields: []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "invoice.sender", "invoice.invoice_status", signingRoot, signatureSender, documents.CDTreePrefix + ".next_version"}, GrantNFTReadAccess: grantAccess, SubmitTokenProof: tokenProof, SubmitNFTReadAccessProof: readAccessProof, @@ -189,7 +213,7 @@ func mintNFTWithProofs(t *testing.T, grantAccess, tokenProof, readAccessProof bo assert.NoError(t, err) cd, err := doc.PackCoreDocument() assert.NoError(t, err) - roleCount := 1 + roleCount := 2 if grantAccess { roleCount++ } diff --git a/p2p/client.go b/p2p/client.go index 9b634ee5a..d333f55e2 100644 --- a/p2p/client.go +++ b/p2p/client.go @@ -13,7 +13,6 @@ import ( "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/identity/ideth" "github.com/centrifuge/go-centrifuge/p2p/common" "github.com/centrifuge/go-centrifuge/version" "github.com/golang/protobuf/proto" @@ -40,7 +39,7 @@ func (s *peer) SendAnchoredDocument(ctx context.Context, receiverID identity.DID if err != nil { return nil, err } - return h.SendAnchoredDocument(localCtx, in, receiverID[:]) + return h.SendAnchoredDocument(localCtx, in, receiverID) } err = s.idService.Exists(ctx, receiverID) @@ -144,8 +143,10 @@ func (s *peer) getSignatureForDocument(ctx context.Context, cd coredocumentpb.Co if err != nil { return nil, err } - - resp, err = h.RequestDocumentSignature(localPeerCtx, &p2ppb.SignatureRequest{Document: &cd}) + if err != nil { + return nil, err + } + resp, err = h.RequestDocumentSignature(localPeerCtx, &p2ppb.SignatureRequest{Document: &cd}, id) if err != nil { return nil, err } @@ -189,7 +190,7 @@ func (s *peer) getSignatureForDocument(ctx context.Context, cd coredocumentpb.Co header = recvEnvelope.Header } - err = validateSignatureResp(s.idService, id, cd.SigningRoot, header, resp) + err = validateSignatureResp(id, header, resp) if err != nil { return nil, err } @@ -271,9 +272,7 @@ func convertClientError(recv *p2ppb.Envelope) error { } func validateSignatureResp( - identityService identity.ServiceDID, receiver identity.DID, - signingRoot []byte, header *p2ppb.Header, resp *p2ppb.SignatureResponse) error { @@ -282,14 +281,9 @@ func validateSignatureResp( return version.IncompatibleVersionError(header.NodeVersion) } - err := ideth.ValidateCentrifugeIDBytes(resp.Signature.EntityId, receiver) + err := identity.ValidateDIDBytes(resp.Signature.SignerId, receiver) if err != nil { return centerrors.New(code.AuthenticationFailed, err.Error()) } - - err = identityService.ValidateSignature(resp.Signature, signingRoot) - if err != nil { - return centerrors.New(code.AuthenticationFailed, fmt.Sprintf("signature invalid with err: %s", err.Error())) - } return nil } diff --git a/p2p/client_integration_test.go b/p2p/client_integration_test.go index 2be3aaec3..92bf7878d 100644 --- a/p2p/client_integration_test.go +++ b/p2p/client_integration_test.go @@ -7,7 +7,6 @@ import ( "flag" "os" "testing" - "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" @@ -84,8 +83,7 @@ func TestClient_GetSignaturesForDocumentValidationCheck(t *testing.T) { dm := prepareDocumentForP2PHandler(t, [][]byte{tc.IdentityID}) signs, _, err := client.GetSignaturesForDocument(ctxh, dm) assert.NoError(t, err) - // one signature would be missing - assert.Equal(t, 0, len(signs)) + assert.Equal(t, 1, len(signs)) } func TestClient_SendAnchoredDocument(t *testing.T) { @@ -138,6 +136,8 @@ func prepareDocumentForP2PHandler(t *testing.T, collaborators [][]byte) document po := new(purchaseorder.PurchaseOrder) err = po.InitPurchaseOrderInput(payalod, defaultDID.String()) assert.NoError(t, err) + err = po.AddUpdateLog(defaultDID) + assert.NoError(t, err) _, err = po.CalculateDataRoot() assert.NoError(t, err) sr, err := po.CalculateSigningRoot() @@ -145,10 +145,10 @@ func prepareDocumentForP2PHandler(t *testing.T, collaborators [][]byte) document s, err := crypto.SignMessage(accKeys[identity.KeyPurposeSigning.Name].PrivateKey, sr, crypto.CurveSecp256K1) assert.NoError(t, err) sig := &coredocumentpb.Signature{ - EntityId: defaultDID[:], - PublicKey: accKeys[identity.KeyPurposeSigning.Name].PublicKey, - Signature: s, - Timestamp: utils.ToTimestamp(time.Now().UTC()), + SignatureId: append(defaultDID[:], accKeys[identity.KeyPurposeSigning.Name].PublicKey...), + SignerId: defaultDID[:], + PublicKey: accKeys[identity.KeyPurposeSigning.Name].PublicKey, + Signature: s, } po.AppendSignatures(sig) _, err = po.CalculateDocumentRoot() diff --git a/p2p/client_test.go b/p2p/client_test.go index d4b453baa..c45a6cd00 100644 --- a/p2p/client_test.go +++ b/p2p/client_test.go @@ -8,6 +8,7 @@ import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" + "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" "github.com/centrifuge/go-centrifuge/errors" @@ -27,6 +28,8 @@ import ( "github.com/stretchr/testify/mock" ) +var did = testingidentity.GenerateRandomDID() + type MockMessenger struct { mock.Mock } @@ -42,70 +45,61 @@ func (mm *MockMessenger) SendMessage(ctx context.Context, p libp2pPeer.ID, pmes } func TestGetSignatureForDocument_fail_connect(t *testing.T) { - centrifugeId, err := identity.NewDIDFromString("0xBAEb33a61f05e6F269f1c4b4CFF91A901B54DaF7") c, err := cfg.GetConfig() assert.NoError(t, err) c = updateKeys(c) ctx := testingconfig.CreateAccountContext(t, c) - idService := getIDMocks(ctx, centrifugeId) + idService := getIDMocks(ctx, did) m := &MockMessenger{} testClient := &peer{config: cfg, idService: idService, mes: m, disablePeerStore: true} - - _, cd := createCDWithEmbeddedPO(t) - + cd, _ := createCDWithEmbeddedPO(t, ctx, did, nil) _, err = p2pcommon.PrepareP2PEnvelope(ctx, c.GetNetworkID(), p2pcommon.MessageTypeRequestSignature, &p2ppb.SignatureRequest{Document: &cd}) assert.NoError(t, err, "signature request could not be created") - m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(¢rifugeId)).Return(nil, errors.New("some error")) - resp, err := testClient.getSignatureForDocument(ctx, cd, centrifugeId) + m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(&did)).Return(nil, errors.New("some error")) + resp, err := testClient.getSignatureForDocument(ctx, cd, did) m.AssertExpectations(t) assert.Error(t, err, "must fail") assert.Nil(t, resp, "must be nil") } func TestGetSignatureForDocument_fail_version_check(t *testing.T) { - centrifugeId, err := identity.NewDIDFromString("0xBAEb33a61f05e6F269f1c4b4CFF91A901B54DaF7") - assert.NoError(t, err) - c, err := cfg.GetConfig() assert.NoError(t, err) c = updateKeys(c) ctx := testingconfig.CreateAccountContext(t, c) - idService := getIDMocks(ctx, centrifugeId) + idService := getIDMocks(ctx, did) m := &MockMessenger{} testClient := &peer{config: cfg, idService: idService, mes: m, disablePeerStore: true} - _, cd := createCDWithEmbeddedPO(t) - + cd, _ := createCDWithEmbeddedPO(t, ctx, did, nil) _, err = p2pcommon.PrepareP2PEnvelope(ctx, c.GetNetworkID(), p2pcommon.MessageTypeRequestSignature, &p2ppb.SignatureRequest{Document: &cd}) assert.NoError(t, err, "signature request could not be created") - m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(¢rifugeId)).Return(testClient.createSignatureResp("", nil), nil) - resp, err := testClient.getSignatureForDocument(ctx, cd, centrifugeId) + m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(&did)).Return(testClient.createSignatureResp("", nil), nil) + resp, err := testClient.getSignatureForDocument(ctx, cd, did) m.AssertExpectations(t) assert.Error(t, err, "must fail") assert.Contains(t, err.Error(), "Incompatible version") assert.Nil(t, resp, "must be nil") } -func TestGetSignatureForDocument_fail_centrifugeId(t *testing.T) { - centrifugeId, err := identity.NewDIDFromString("0xBAEb33a61f05e6F269f1c4b4CFF91A901B54DaF7") +func TestGetSignatureForDocument_fail_did(t *testing.T) { c, err := cfg.GetConfig() assert.NoError(t, err) c = updateKeys(c) ctx := testingconfig.CreateAccountContext(t, c) - idService := getIDMocks(ctx, centrifugeId) + idService := getIDMocks(ctx, did) m := &MockMessenger{} testClient := &peer{config: cfg, idService: idService, mes: m, disablePeerStore: true} - _, cd := createCDWithEmbeddedPO(t) - + cd, _ := createCDWithEmbeddedPO(t, ctx, did, nil) _, err = p2pcommon.PrepareP2PEnvelope(ctx, c.GetNetworkID(), p2pcommon.MessageTypeRequestSignature, &p2ppb.SignatureRequest{Document: &cd}) assert.NoError(t, err, "signature request could not be created") randomBytes := utils.RandomSlice(identity.DIDLength) - signature := &coredocumentpb.Signature{EntityId: randomBytes, PublicKey: utils.RandomSlice(32)} - m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(¢rifugeId)).Return(testClient.createSignatureResp(version.GetVersion().String(), signature), nil) + signature := &coredocumentpb.Signature{SignatureId: utils.RandomSlice(52), SignerId: randomBytes, PublicKey: utils.RandomSlice(32)} + m.On("SendMessage", ctx, mock.Anything, mock.Anything, p2pcommon.ProtocolForDID(&did)).Return(testClient.createSignatureResp(version.GetVersion().String(), signature), nil) - resp, err := testClient.getSignatureForDocument(ctx, cd, centrifugeId) + resp, err := testClient.getSignatureForDocument(ctx, cd, did) m.AssertExpectations(t) assert.Nil(t, resp, "must be nil") @@ -114,10 +108,10 @@ func TestGetSignatureForDocument_fail_centrifugeId(t *testing.T) { } -func getIDMocks(ctx context.Context, centrifugeId identity.DID) *testingcommons.MockIdentityService { +func getIDMocks(ctx context.Context, did identity.DID) *testingcommons.MockIdentityService { idService := &testingcommons.MockIdentityService{} - idService.On("CurrentP2PKey", centrifugeId).Return("5dsgvJGnvAfiR3K6HCBc4hcokSfmjj", nil) - idService.On("Exists", ctx, centrifugeId).Return(nil) + idService.On("CurrentP2PKey", did).Return("5dsgvJGnvAfiR3K6HCBc4hcokSfmjj", nil) + idService.On("Exists", ctx, did).Return(nil) return idService } @@ -143,17 +137,34 @@ func (s *peer) createSignatureResp(centNodeVer string, signature *coredocumentpb return &protocolpb.P2PEnvelope{Body: reqB} } -func createCDWithEmbeddedPO(t *testing.T) (documents.Model, coredocumentpb.CoreDocument) { +func createCDWithEmbeddedPO(t *testing.T, ctx context.Context, cid identity.DID, collaborators []identity.DID) (coredocumentpb.CoreDocument, documents.Model) { po := new(purchaseorder.PurchaseOrder) - err := po.InitPurchaseOrderInput(testingdocuments.CreatePOPayload(), testingidentity.GenerateRandomDID().String()) + data := testingdocuments.CreatePOPayload() + if len(collaborators) > 0 { + var cs []string + for _, c := range collaborators { + cs = append(cs, c.String()) + } + data.Collaborators = cs + } + err := po.InitPurchaseOrderInput(data, cid.String()) assert.NoError(t, err) _, err = po.CalculateDataRoot() assert.NoError(t, err) - _, err = po.CalculateSigningRoot() + sr, err := po.CalculateSigningRoot() + assert.NoError(t, err) + + acc, err := contextutil.Account(ctx) assert.NoError(t, err) + + sig, err := acc.SignMsg(sr) + assert.NoError(t, err) + + po.AppendSignatures(sig) _, err = po.CalculateDocumentRoot() assert.NoError(t, err) cd, err := po.PackCoreDocument() assert.NoError(t, err) - return po, cd + + return cd, po } diff --git a/p2p/common/protocol.go b/p2p/common/protocol.go index 9073779ee..ca0b356ec 100644 --- a/p2p/common/protocol.go +++ b/p2p/common/protocol.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "strings" + "time" + + "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/contextutil" @@ -114,11 +117,18 @@ func PrepareP2PEnvelope(ctx context.Context, networkID uint32, messageType Messa if err != nil { return nil, err } + + tm, err := utils.ToTimestamp(time.Now().UTC()) + if err != nil { + return nil, err + } + p2pheader := &p2ppb.Header{ SenderId: centIDBytes, NodeVersion: version.GetVersion().String(), NetworkIdentifier: networkID, Type: messageType.String(), + Timestamp: tm, } body, err := proto.Marshal(mes) diff --git a/p2p/receiver/handler.go b/p2p/receiver/handler.go index a3ae67486..061fd474b 100644 --- a/p2p/receiver/handler.go +++ b/p2p/receiver/handler.go @@ -68,8 +68,8 @@ func (srv *Handler) HandleInterceptor(ctx context.Context, peer peer.ID, protoc if err != nil { return convertToErrorEnvelop(err) } - fromID := identity.NewDIDFromBytes(envelope.Header.SenderId) - err = srv.handshakeValidator.Validate(envelope.Header, &fromID, &peer) + collaborator := identity.NewDIDFromBytes(envelope.Header.SenderId) + err = srv.handshakeValidator.Validate(envelope.Header, &collaborator, &peer) if err != nil { return convertToErrorEnvelop(err) } @@ -95,7 +95,8 @@ func (srv *Handler) HandleRequestDocumentSignature(ctx context.Context, peer pee return convertToErrorEnvelop(err) } - res, err := srv.RequestDocumentSignature(ctx, req) + collaborator := identity.NewDIDFromBytes(msg.Header.SenderId) + res, err := srv.RequestDocumentSignature(ctx, req, collaborator) if err != nil { return convertToErrorEnvelop(err) } @@ -117,7 +118,7 @@ func (srv *Handler) HandleRequestDocumentSignature(ctx context.Context, peer pee // Document signing root will be recalculated and verified // Existing signatures on the document will be verified // Document will be stored to the repository for state management -func (srv *Handler) RequestDocumentSignature(ctx context.Context, sigReq *p2ppb.SignatureRequest) (*p2ppb.SignatureResponse, error) { +func (srv *Handler) RequestDocumentSignature(ctx context.Context, sigReq *p2ppb.SignatureRequest, collaborator identity.DID) (*p2ppb.SignatureResponse, error) { if sigReq == nil || sigReq.Document == nil { return nil, errors.New("nil document provided") } @@ -127,7 +128,7 @@ func (srv *Handler) RequestDocumentSignature(ctx context.Context, sigReq *p2ppb. return nil, errors.New("failed to derive from core doc: %v", err) } - signature, err := srv.docSrv.RequestDocumentSignature(ctx, model) + signature, err := srv.docSrv.RequestDocumentSignature(ctx, model, collaborator) if err != nil { return nil, centerrors.New(code.Unknown, err.Error()) } @@ -143,7 +144,8 @@ func (srv *Handler) HandleSendAnchoredDocument(ctx context.Context, peer peer.ID return convertToErrorEnvelop(err) } - res, err := srv.SendAnchoredDocument(ctx, m, msg.Header.SenderId) + collaborator := identity.NewDIDFromBytes(msg.Header.SenderId) + res, err := srv.SendAnchoredDocument(ctx, m, collaborator) if err != nil { return convertToErrorEnvelop(err) } @@ -162,7 +164,7 @@ func (srv *Handler) HandleSendAnchoredDocument(ctx context.Context, peer peer.ID } // SendAnchoredDocument receives a new anchored document, validates and updates the document in DB -func (srv *Handler) SendAnchoredDocument(ctx context.Context, docReq *p2ppb.AnchorDocumentRequest, senderID []byte) (*p2ppb.AnchorDocumentResponse, error) { +func (srv *Handler) SendAnchoredDocument(ctx context.Context, docReq *p2ppb.AnchorDocumentRequest, collaborator identity.DID) (*p2ppb.AnchorDocumentResponse, error) { if docReq == nil || docReq.Document == nil { return nil, errors.New("nil document provided") } @@ -172,7 +174,7 @@ func (srv *Handler) SendAnchoredDocument(ctx context.Context, docReq *p2ppb.Anch return nil, errors.New("failed to derive from core doc: %v", err) } - err = srv.docSrv.ReceiveAnchoredDocument(ctx, model, senderID) + err = srv.docSrv.ReceiveAnchoredDocument(ctx, model, collaborator) if err != nil { return nil, centerrors.New(code.Unknown, err.Error()) } diff --git a/p2p/receiver/handler_integration_test.go b/p2p/receiver/handler_integration_test.go index 97e2e12b5..aa3b14412 100644 --- a/p2p/receiver/handler_integration_test.go +++ b/p2p/receiver/handler_integration_test.go @@ -5,27 +5,22 @@ package receiver_test import ( "context" "flag" + "fmt" "math/big" "os" "testing" - "time" - - "github.com/centrifuge/go-centrifuge/crypto/secp256k1" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/crypto" - - "github.com/centrifuge/go-centrifuge/config/configstore" - "github.com/ethereum/go-ethereum/common" - "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/testingbootstrap" "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/config/configstore" "github.com/centrifuge/go-centrifuge/contextutil" + "github.com/centrifuge/go-centrifuge/crypto" cented25519 "github.com/centrifuge/go-centrifuge/crypto/ed25519" + "github.com/centrifuge/go-centrifuge/crypto/secp256k1" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" "github.com/centrifuge/go-centrifuge/identity" @@ -35,7 +30,10 @@ import ( "github.com/centrifuge/go-centrifuge/storage" "github.com/centrifuge/go-centrifuge/testingutils/config" "github.com/centrifuge/go-centrifuge/testingutils/documents" + "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" ) @@ -69,19 +67,17 @@ func TestMain(m *testing.M) { func TestHandler_GetDocument_nonexistentIdentifier(t *testing.T) { b := utils.RandomSlice(32) - centrifugeId := createIdentity(t) req := &p2ppb.GetDocumentRequest{DocumentIdentifier: b} - resp, err := handler.GetDocument(context.Background(), req, centrifugeId) + resp, err := handler.GetDocument(context.Background(), req, defaultDID) assert.Error(t, err, "must return error") assert.Nil(t, resp, "must be nil") } func TestHandler_HandleInterceptorReqSignature(t *testing.T) { - centID := createIdentity(t) tc, err := configstore.NewAccount("main", cfg) assert.Nil(t, err) acc := tc.(*configstore.Account) - acc.IdentityID = centID[:] + acc.IdentityID = defaultDID[:] ctxh, err := contextutil.New(context.Background(), acc) assert.Nil(t, err) _, err = cfgService.CreateAccount(acc) @@ -97,7 +93,7 @@ func TestHandler_HandleInterceptorReqSignature(t *testing.T) { peerID, err := cented25519.PublicKeyToP2PKey(bPk) assert.NoError(t, err) - p2pResp, err := handler.HandleInterceptor(ctxh, peerID, p2pcommon.ProtocolForDID(¢ID), p2pEnv) + p2pResp, err := handler.HandleInterceptor(ctxh, peerID, p2pcommon.ProtocolForDID(&defaultDID), p2pEnv) assert.Nil(t, err, "must be nil") assert.NotNil(t, p2pResp, "must be non nil") resp := resolveSignatureResponse(t, p2pResp) @@ -106,59 +102,48 @@ func TestHandler_HandleInterceptorReqSignature(t *testing.T) { assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), cd.SigningRoot), "signature must be valid") } -func TestHandler_RequestDocumentSignature_AlreadyExists(t *testing.T) { - _, cd := prepareDocumentForP2PHandler(t, nil) - ctxh := testingconfig.CreateAccountContext(t, cfg) - resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.Nil(t, err, "must be nil") - assert.NotNil(t, resp, "must be non nil") +func TestHandler_RequestDocumentSignature(t *testing.T) { + tc, err := configstore.NewAccount("main", cfg) + assert.Nil(t, err) + acc := tc.(*configstore.Account) + acc.IdentityID = defaultDID[:] - resp, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.NotNil(t, err, "must not be nil") - assert.Contains(t, err.Error(), storage.ErrRepositoryModelCreateKeyExists.Error()) -} + ctxh, err := contextutil.New(context.Background(), acc) + assert.Nil(t, err) -func TestHandler_RequestDocumentSignature_UpdateSucceeds(t *testing.T) { - ctxh := testingconfig.CreateAccountContext(t, cfg) po, cd := prepareDocumentForP2PHandler(t, nil) - resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.Nil(t, err, "must be nil") - assert.NotNil(t, resp, "must be non nil") - assert.NotNil(t, resp.Signature.Signature, "must be non nil") - sig := resp.Signature - assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), cd.SigningRoot), "signature must be valid") - //Update document - po, cd = updateDocumentForP2Phandler(t, po) - resp, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.Nil(t, err, "must be nil") - assert.NotNil(t, resp, "must be non nil") - assert.NotNil(t, resp.Signature.Signature, "must be non nil") - sig = resp.Signature - assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), cd.SigningRoot), "signature must be valid") -} -func TestHandler_RequestDocumentSignatureFirstTimeOnUpdatedDocument(t *testing.T) { - ctxh := testingconfig.CreateAccountContext(t, cfg) - po, cd := prepareDocumentForP2PHandler(t, nil) - po, cd = updateDocumentForP2Phandler(t, po) - assert.NotEqual(t, cd.DocumentIdentifier, cd.CurrentVersion) - resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.Nil(t, err, "must be nil") - assert.NotNil(t, resp, "must be non nil") - assert.NotNil(t, resp.Signature.Signature, "must be non nil") - sig := resp.Signature - assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), cd.SigningRoot), "signature must be valid") -} + // nil sigRequest + id2 := testingidentity.GenerateRandomDID() + _, err = handler.RequestDocumentSignature(ctxh, nil, defaultDID) + assert.Error(t, err) + assert.Contains(t, err.Error(), "nil document provided") -func TestHandler_RequestDocumentSignature(t *testing.T) { - ctxh := testingconfig.CreateAccountContext(t, cfg) - _, cd := prepareDocumentForP2PHandler(t, nil) - resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) - assert.Nil(t, err, "must be nil") + // requestDocumentSignature, no previous versions + _, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}, defaultDID) + assert.NoError(t, err) + + // we can update the document so that there are two versions in the repo + _, ncd := updateDocumentForP2Phandler(t, po) + assert.NotEqual(t, cd.DocumentIdentifier, ncd.CurrentVersion) + + // invalid transition for non-collaborator id + _, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &ncd}, id2) + assert.Error(t, err) + + // valid transition for collaborator id + resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &ncd}, defaultDID) + fmt.Println(ncd.PreviousVersion, ncd.CurrentVersion) + assert.NoError(t, err) assert.NotNil(t, resp, "must be non nil") assert.NotNil(t, resp.Signature.Signature, "must be non nil") sig := resp.Signature - assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), cd.SigningRoot), "signature must be valid") + assert.True(t, secp256k1.VerifySignatureWithAddress(common.BytesToAddress(sig.PublicKey).String(), hexutil.Encode(sig.Signature), ncd.SigningRoot), "signature must be valid") + + // document already exists + _, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}, defaultDID) + assert.NotNil(t, err, "must not be nil") + assert.Contains(t, err.Error(), storage.ErrRepositoryModelCreateKeyExists.Error()) } func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { @@ -168,8 +153,10 @@ func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { // Anchor document accDID, err := contextutil.AccountDID(ctx) assert.NoError(t, err) - anchorIDTyped, _ := anchors.ToAnchorID(cd.CurrentPreimage) - docRootTyped, _ := anchors.ToDocumentRoot(cd.DocumentRoot) + anchorIDTyped, err := anchors.ToAnchorID(cd.CurrentPreimage) + assert.NoError(t, err) + docRootTyped, err := anchors.ToDocumentRoot(cd.DocumentRoot) + assert.NoError(t, err) anchorConfirmations, err := anchorRepo.CommitAnchor(ctx, anchorIDTyped, docRootTyped, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}) assert.Nil(t, err) @@ -177,7 +164,7 @@ func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { watchCommittedAnchor := <-anchorConfirmations assert.True(t, watchCommittedAnchor, "No error should be thrown by context") - anchorResp, err := handler.SendAnchoredDocument(ctx, &p2ppb.AnchorDocumentRequest{Document: &cd}, accDID[:]) + anchorResp, err := handler.SendAnchoredDocument(ctx, &p2ppb.AnchorDocumentRequest{Document: &cd}, accDID) assert.Error(t, err) assert.Contains(t, err.Error(), storage.ErrRepositoryModelUpdateKeyNotFound.Error()) assert.Nil(t, anchorResp) @@ -186,8 +173,9 @@ func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { func TestHandler_SendAnchoredDocument_EmptyDocument(t *testing.T) { ctxh := testingconfig.CreateAccountContext(t, cfg) id, err := cfg.GetIdentityID() + collaborator := identity.NewDIDFromBytes(id) assert.NoError(t, err) - resp, err := handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{}, id) + resp, err := handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{}, collaborator) assert.NotNil(t, err) assert.Nil(t, resp, "must be nil") } @@ -195,26 +183,31 @@ func TestHandler_SendAnchoredDocument_EmptyDocument(t *testing.T) { func TestHandler_SendAnchoredDocument(t *testing.T) { tc, err := configstore.NewAccount("main", cfg) assert.Nil(t, err) - centrifugeId := createIdentity(t) acc := tc.(*configstore.Account) - acc.IdentityID = centrifugeId[:] + acc.IdentityID = defaultDID[:] - ctxh, err := contextutil.New(context.Background(), tc) + ctxh, err := contextutil.New(context.Background(), acc) assert.Nil(t, err) po, cd := prepareDocumentForP2PHandler(t, nil) - resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}) + resp, err := handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &cd}, defaultDID) assert.Nil(t, err) assert.NotNil(t, resp) // Add signature received po.AppendSignatures(resp.Signature) + + // Since we have changed the coredocument by adding signatures lets generate salts again + po.Document.SignatureDataSalts = nil tree, err := po.DocumentRootTree() po.Document.DocumentRoot = tree.RootHash() // Anchor document - anchorIDTyped, _ := anchors.ToAnchorID(po.Document.CurrentPreimage) - docRootTyped, _ := anchors.ToDocumentRoot(po.Document.DocumentRoot) + anchorIDTyped, err := anchors.ToAnchorID(po.Document.CurrentPreimage) + assert.NoError(t, err) + docRootTyped, err := anchors.ToDocumentRoot(po.Document.DocumentRoot) + assert.NoError(t, err) + anchorConfirmations, err := anchorRepo.CommitAnchor(ctxh, anchorIDTyped, docRootTyped, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}) assert.Nil(t, err) @@ -222,7 +215,44 @@ func TestHandler_SendAnchoredDocument(t *testing.T) { assert.True(t, watchCommittedAnchor, "No error should be thrown by context") cd, err = po.PackCoreDocument() assert.NoError(t, err) - anchorResp, err := handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{Document: &cd}, centrifugeId[:]) + + // this should succeed since this is the first document version + anchorResp, err := handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{Document: &cd}, defaultDID) + assert.Nil(t, err) + assert.NotNil(t, anchorResp, "must be non nil") + assert.True(t, anchorResp.Accepted) + + // update the document + npo, ncd := updateDocumentForP2Phandler(t, po) + resp, err = handler.RequestDocumentSignature(ctxh, &p2ppb.SignatureRequest{Document: &ncd}, defaultDID) + assert.Nil(t, err) + assert.NotNil(t, resp) + + // Add signature received + npo.AppendSignatures(resp.Signature) + tree, err = npo.DocumentRootTree() + npo.Document.DocumentRoot = tree.RootHash() + + // Anchor document + anchorIDTyped, err = anchors.ToAnchorID(npo.Document.CurrentPreimage) + assert.NoError(t, err) + docRootTyped, err = anchors.ToDocumentRoot(npo.Document.DocumentRoot) + assert.NoError(t, err) + anchorConfirmations, err = anchorRepo.CommitAnchor(ctxh, anchorIDTyped, docRootTyped, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}) + assert.Nil(t, err) + + watchCommittedAnchor = <-anchorConfirmations + assert.True(t, watchCommittedAnchor, "No error should be thrown by context") + ncd, err = npo.PackCoreDocument() + assert.NoError(t, err) + + // transition failure for random ID + id := testingidentity.GenerateRandomDID() + _, err = handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{Document: &ncd}, id) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid document state transition") + + anchorResp, err = handler.SendAnchoredDocument(ctxh, &p2ppb.AnchorDocumentRequest{Document: &ncd}, defaultDID) assert.Nil(t, err) assert.NotNil(t, anchorResp, "must be non nil") assert.True(t, anchorResp.Accepted) @@ -248,13 +278,13 @@ func createIdentity(t *testing.T) identity.DID { assert.NoError(t, err) pk, err := utils.SliceToByte32(accKeys[identity.KeyPurposeP2PDiscovery.Name].PublicKey) assert.NoError(t, err) - keyDID := identity.NewKey(pk, &(identity.KeyPurposeP2PDiscovery.Value), big.NewInt(identity.KeyTypeECDSA)) + keyDID := identity.NewKey(pk, &(identity.KeyPurposeP2PDiscovery.Value), big.NewInt(identity.KeyTypeECDSA), 0) err = idService.AddKey(ctx, keyDID) assert.Nil(t, err, "should not error out when adding key to identity") sPk, err := utils.SliceToByte32(accKeys[identity.KeyPurposeSigning.Name].PublicKey) assert.NoError(t, err) - keyDID = identity.NewKey(sPk, &(identity.KeyPurposeSigning.Value), big.NewInt(identity.KeyTypeECDSA)) + keyDID = identity.NewKey(sPk, &(identity.KeyPurposeSigning.Value), big.NewInt(identity.KeyTypeECDSA), 0) err = idService.AddKey(ctx, keyDID) assert.Nil(t, err, "should not error out when adding key to identity") @@ -275,6 +305,8 @@ func prepareDocumentForP2PHandler(t *testing.T, po *purchaseorder.PurchaseOrder) err = po.InitPurchaseOrderInput(payload, defaultDID.String()) assert.NoError(t, err) } + err = po.AddUpdateLog(defaultDID) + assert.NoError(t, err) _, err = po.CalculateDataRoot() assert.NoError(t, err) sr, err := po.CalculateSigningRoot() @@ -282,10 +314,10 @@ func prepareDocumentForP2PHandler(t *testing.T, po *purchaseorder.PurchaseOrder) s, err := crypto.SignMessage(accKeys[identity.KeyPurposeSigning.Name].PrivateKey, sr, crypto.CurveSecp256K1) assert.NoError(t, err) sig := &coredocumentpb.Signature{ - EntityId: defaultDID[:], - PublicKey: accKeys[identity.KeyPurposeSigning.Name].PublicKey, - Signature: s, - Timestamp: utils.ToTimestamp(time.Now().UTC()), + SignatureId: append(defaultDID[:], accKeys[identity.KeyPurposeSigning.Name].PublicKey...), + SignerId: defaultDID[:], + PublicKey: accKeys[identity.KeyPurposeSigning.Name].PublicKey, + Signature: s, } po.AppendSignatures(sig) _, err = po.CalculateDocumentRoot() @@ -296,7 +328,7 @@ func prepareDocumentForP2PHandler(t *testing.T, po *purchaseorder.PurchaseOrder) } func updateDocumentForP2Phandler(t *testing.T, po *purchaseorder.PurchaseOrder) (*purchaseorder.PurchaseOrder, coredocumentpb.CoreDocument) { - cd, err := po.CoreDocument.PrepareNewVersion(nil, true) + cd, err := po.CoreDocument.PrepareNewVersion(nil, true, nil) assert.NoError(t, err) po.CoreDocument = cd return prepareDocumentForP2PHandler(t, po) diff --git a/p2p/receiver/handler_test.go b/p2p/receiver/handler_test.go index f553f2317..0400d9657 100644 --- a/p2p/receiver/handler_test.go +++ b/p2p/receiver/handler_test.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "os" "testing" + "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/anchors" @@ -22,10 +23,12 @@ import ( "github.com/centrifuge/go-centrifuge/protobufs/gen/go/protocol" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/storage/leveldb" - testingcommons "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/testingutils/config" "github.com/centrifuge/go-centrifuge/testingutils/documents" + "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/transactions/txv1" + "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/go-centrifuge/version" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/proto" @@ -77,8 +80,9 @@ func TestMain(m *testing.M) { } func TestHandler_RequestDocumentSignature_nilDocument(t *testing.T) { + id := testingidentity.GenerateRandomDID() req := &p2ppb.SignatureRequest{} - resp, err := handler.RequestDocumentSignature(context.Background(), req) + resp, err := handler.RequestDocumentSignature(context.Background(), req, id) assert.Error(t, err, "must return error") assert.Nil(t, resp, "must be nil") } @@ -178,7 +182,7 @@ func TestHandler_HandleInterceptor_NilDocument(t *testing.T) { func TestHandler_HandleInterceptor_getServiceAndModel_fail(t *testing.T) { ctx := testingconfig.CreateAccountContext(t, cfg) - cd, err := documents.NewCoreDocumentWithCollaborators(nil) + cd, err := documents.NewCoreDocumentWithCollaborators(nil, nil) assert.NoError(t, err) req := &p2ppb.AnchorDocumentRequest{Document: &cd.Document} p2pEnv, err := p2pcommon.PrepareP2PEnvelope(ctx, cfg.GetNetworkID(), p2pcommon.MessageTypeSendAnchoredDoc, req) @@ -192,22 +196,24 @@ func TestHandler_HandleInterceptor_getServiceAndModel_fail(t *testing.T) { } func TestP2PService_basicChecks(t *testing.T) { + tm, err := utils.ToTimestamp(time.Now()) + assert.NoError(t, err) tests := []struct { header *p2ppb.Header err error }{ { - header: &p2ppb.Header{NodeVersion: "someversion", NetworkIdentifier: 12}, + header: &p2ppb.Header{NodeVersion: "someversion", NetworkIdentifier: 12, Timestamp: tm}, err: errors.AppendError(version.IncompatibleVersionError("someversion"), incompatibleNetworkError(cfg.GetNetworkID(), 12)), }, { - header: &p2ppb.Header{NodeVersion: "0.0.1", NetworkIdentifier: 12}, + header: &p2ppb.Header{NodeVersion: "0.0.1", NetworkIdentifier: 12, Timestamp: tm}, err: errors.AppendError(incompatibleNetworkError(cfg.GetNetworkID(), 12), nil), }, { - header: &p2ppb.Header{NodeVersion: version.GetVersion().String(), NetworkIdentifier: cfg.GetNetworkID()}, + header: &p2ppb.Header{NodeVersion: version.GetVersion().String(), NetworkIdentifier: cfg.GetNetworkID(), Timestamp: tm}, }, } diff --git a/p2p/receiver/validator.go b/p2p/receiver/validator.go index 869aa7dcb..6049aeaa4 100644 --- a/p2p/receiver/validator.go +++ b/p2p/receiver/validator.go @@ -4,12 +4,11 @@ import ( "context" "fmt" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/code" "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/version" libp2pPeer "github.com/libp2p/go-libp2p-peer" ) @@ -89,7 +88,8 @@ func peerValidator(idService identity.ServiceDID) Validator { if err != nil { return err } - return idService.ValidateKey(context.Background(), *centID, idKey, &(identity.KeyPurposeP2PDiscovery.Value)) + + return idService.ValidateKey(context.Background(), *centID, idKey, &(identity.KeyPurposeP2PDiscovery.Value), nil) }) } diff --git a/p2p/receiver/validator_test.go b/p2p/receiver/validator_test.go index 73c29a865..16e224b1f 100644 --- a/p2p/receiver/validator_test.go +++ b/p2p/receiver/validator_test.go @@ -4,6 +4,9 @@ package receiver import ( "testing" + "time" + + "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/go-centrifuge/identity" @@ -79,8 +82,13 @@ func TestValidate_peerValidator(t *testing.T) { err := sv.Validate(nil, nil, nil) assert.Error(t, err) + tm, err := utils.ToTimestamp(time.Now()) + assert.NoError(t, err) + // Nil centID - header := &p2ppb.Header{} + header := &p2ppb.Header{ + Timestamp: tm, + } err = sv.Validate(header, nil, nil) assert.Error(t, err) @@ -104,13 +112,16 @@ func TestValidate_handshakeValidator(t *testing.T) { idService := &testingcommons.MockIdentityService{} hv := HandshakeValidator(cfg.GetNetworkID(), idService) + tm, err := utils.ToTimestamp(time.Now()) + assert.NoError(t, err) // Incompatible version network and wrong signature header := &p2ppb.Header{ NodeVersion: "version", NetworkIdentifier: 52, + Timestamp: tm, } - err := hv.Validate(header, nil, nil) + err = hv.Validate(header, nil, nil) assert.NotNil(t, err) // Incompatible version, correct network diff --git a/p2p/server.go b/p2p/server.go index 19aa529d4..3a4f5e62e 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -7,7 +7,7 @@ import ( "time" "github.com/centrifuge/go-centrifuge/config" - cented25519 "github.com/centrifuge/go-centrifuge/crypto/ed25519" + crypto2 "github.com/centrifuge/go-centrifuge/crypto" "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/p2p/common" @@ -73,7 +73,7 @@ func (s *peer) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- // Make a host that listens on the given multiaddress // first obtain the keys configured - priv, pub, err := s.createSigningKey(nc.GetP2PKeyPair()) + priv, pub, err := crypto2.ObtainP2PKeypair(nc.GetP2PKeyPair()) if err != nil { startupErr <- err return @@ -120,26 +120,6 @@ func (s *peer) InitProtocolForDID(DID *identity.DID) { s.mes.Init(p) } -func (s *peer) createSigningKey(pubKey, privKey string) (priv crypto.PrivKey, pub crypto.PubKey, err error) { - // Create the signing key for the host - publicKey, privateKey, err := cented25519.GetSigningKeyPair(pubKey, privKey) - if err != nil { - return nil, nil, errors.New("failed to get keys: %v", err) - } - - var key []byte - key = append(key, privateKey...) - key = append(key, publicKey...) - - priv, err = crypto.UnmarshalEd25519PrivateKey(key) - if err != nil { - return nil, nil, err - } - - pub = priv.GetPublic() - return priv, pub, nil -} - // makeBasicHost creates a LibP2P host with a peer ID listening on the given port func makeBasicHost(priv crypto.PrivKey, pub crypto.PubKey, externalIP string, listenPort int) (host.Host, error) { // Obtain Peer ID from public key diff --git a/p2p/server_test.go b/p2p/server_test.go index 212048ee4..0a18dd4fa 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -15,6 +15,7 @@ import ( "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/testlogging" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/config/configstore" + "github.com/centrifuge/go-centrifuge/crypto" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" @@ -108,9 +109,8 @@ func TestCentP2PServer_makeBasicHostNoExternalIP(t *testing.T) { assert.NoError(t, err) c = updateKeys(c) listenPort := 38202 - cp2p := &peer{config: cfg} pu, pr := c.GetP2PKeyPair() - priv, pub, err := cp2p.createSigningKey(pu, pr) + priv, pub, err := crypto.ObtainP2PKeypair(pu, pr) h, err := makeBasicHost(priv, pub, "", listenPort) assert.Nil(t, err) assert.NotNil(t, h) @@ -122,9 +122,8 @@ func TestCentP2PServer_makeBasicHostWithExternalIP(t *testing.T) { c = updateKeys(c) externalIP := "100.100.100.100" listenPort := 38202 - cp2p := &peer{config: cfg} pu, pr := c.GetP2PKeyPair() - priv, pub, err := cp2p.createSigningKey(pu, pr) + priv, pub, err := crypto.ObtainP2PKeypair(pu, pr) h, err := makeBasicHost(priv, pub, externalIP, listenPort) assert.Nil(t, err) assert.NotNil(t, h) @@ -140,9 +139,8 @@ func TestCentP2PServer_makeBasicHostWithWrongExternalIP(t *testing.T) { c = updateKeys(c) externalIP := "100.200.300.400" listenPort := 38202 - cp2p := &peer{config: cfg} pu, pr := c.GetP2PKeyPair() - priv, pub, err := cp2p.createSigningKey(pu, pr) + priv, pub, err := crypto.ObtainP2PKeypair(pu, pr) h, err := makeBasicHost(priv, pub, externalIP, listenPort) assert.NotNil(t, err) assert.Nil(t, h) diff --git a/resources/data.go b/resources/data.go index acd4a3661..b0f159d5a 100644 --- a/resources/data.go +++ b/resources/data.go @@ -69,7 +69,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _goCentrifugeBuildConfigsDefault_configYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x58\x5b\x73\xdb\x36\x16\x7e\xd7\xaf\x38\xe3\xbc\xb4\x33\x4b\x99\x77\x51\x9a\xe9\xec\xf8\x92\x8b\x1b\xc7\x95\x6d\xb9\x6e\xfc\xb2\x39\x04\x0e\x25\xc4\x14\xc0\x00\xa0\x2e\xf9\xf5\x3b\x00\x29\xd5\x8e\x63\x77\xb7\x9d\xee\xfa\xc5\x14\x80\x73\xff\xce\x87\xcb\x2b\x38\xa5\x0a\xdb\xda\x02\xa7\x15\xd5\xaa\x59\x92\xb4\x60\xc9\x58\x49\x16\x70\x8e\x42\x1a\x0b\x5a\xc8\x7b\x2a\xb7\x03\x46\xd2\x6a\x51\xb5\x73\xba\x20\xbb\x56\xfa\x7e\x02\xba\x35\x46\xa0\x5c\x88\xba\x1e\x78\x65\x42\x12\xd8\x05\x01\xef\xf5\xca\x6e\xa5\x01\xbb\x40\x0b\x27\x7b\x0d\xb0\x44\x21\xad\xd3\x3f\xd8\x2d\x99\x0c\x00\x5e\xc1\xb9\x62\x58\x7b\x17\x84\x9c\x03\x53\xd2\x6a\x64\x16\x90\x73\x4d\xc6\x90\x01\x49\xc4\xc1\x2a\x28\x09\x0c\x59\x58\x0b\xbb\x00\x92\x2b\x58\xa1\x16\x58\xd6\x64\x86\x03\xd8\xc9\x3b\x95\x00\x82\x4f\x20\x49\x12\xff\x4d\x76\x41\x9a\xda\x65\x1f\xc1\x19\x9f\x40\x91\x14\xdd\x5c\xa9\x94\x35\x56\x63\x33\x25\xd2\xa6\x93\x0d\xe0\xe0\x50\x34\xe9\x61\x14\x8f\x86\xe1\x30\x1c\x46\x87\x96\x35\x87\x49\x11\x87\xf1\xa1\x68\x2a\x73\x78\xb9\x9c\x5d\x6e\xca\xf5\x7d\x7b\xf7\xf1\xe3\x69\xd5\x7e\x9d\x95\x9b\xd7\x47\x57\x34\xbb\x38\x39\x57\x5f\xb7\xdb\x2c\x2b\x56\x97\x72\xfe\xeb\x6a\xfa\xe1\xf3\xf9\xc7\xfb\x83\x3f\x50\x9a\xec\x94\xfe\x5a\xe5\xaf\x2f\xf2\xe5\xfd\x97\x5b\xfa\x7c\xfb\xfe\x36\xfe\x32\x6d\xa3\xfc\xb7\x86\xbf\x4d\xee\x7f\x56\xd1\x2c\x59\x2e\x70\x31\x3d\xce\xae\x29\x93\x51\xa7\x74\x97\xaa\xa3\x5d\xa6\xba\x00\x5c\xf8\x24\xad\xb0\xdb\x37\xc8\xac\xd2\xdb\x09\x1c\x1c\xf4\x33\x28\xd9\x42\xe9\x2b\x6a\x94\x11\xdf\x4c\x35\xb8\x75\x58\xf8\xa5\xac\xc5\x1c\xad\x50\xd2\xcf\xf9\x0a\x7d\x40\x21\xbf\x8b\x97\xbe\x90\xf0\xc3\x55\x07\x98\x1f\x07\xf0\x10\x20\x9d\x3f\xaf\xe0\xa2\x5d\x92\x16\x0c\xce\x4e\x41\x55\x1e\x2c\x0f\x60\xd1\xeb\xd8\xd7\x2d\x8b\x7a\xa9\xe3\x5d\x71\xa0\x16\xc6\x3a\x49\xa9\x38\x3d\xc5\x55\xa3\xd5\x4a\xf8\x09\xe5\x75\x3f\x70\x60\xe7\xe8\x1f\x16\x3b\xc9\x86\x71\x9c\x0d\xe3\x30\x1c\xa6\xf1\xb7\x05\x8f\xe2\xd3\xe4\xbd\x52\xb7\xe7\x42\xb0\xcb\x5f\xd7\xb3\xc5\xec\xf8\x63\xbe\x79\xcf\xa6\xea\xbc\xca\xaf\x2e\x3f\xfe\xfc\xa6\x59\x57\x91\x1e\x65\xeb\xf3\x4d\x7c\x77\x95\x34\x27\x3c\x3a\xf8\x9e\xfa\x22\x1f\xc6\x51\xf8\x9c\xfa\xcb\xbb\x0f\x47\xc5\xdb\xe9\x3b\xbd\x7a\x7d\x77\x3c\x5e\xf3\x7b\x75\xc3\x8e\x8e\x96\x27\x77\xef\x9a\x31\x6d\xb7\x77\xe9\xf5\xeb\x62\xfe\x46\x27\x8b\xd9\xc5\x6f\x07\x7d\x8e\x5e\xf7\xe0\xde\x57\xe2\xec\x14\x02\xe8\xab\xf1\x1c\xfc\xd3\x5e\xf8\x1c\x5d\x7a\x80\x53\x53\xab\x2d\x71\xb8\x5e\xa2\xb6\x70\xd2\xa3\xca\x40\xa5\xb4\x4f\xe8\x5c\xac\x48\x3e\x4a\xe5\x53\xe4\xc1\xb3\xd0\x0b\x37\x55\x45\x61\x1e\xc5\x61\x98\x13\x71\x24\x8e\x51\x51\x30\x3e\xe6\x55\x96\x66\xa3\x14\xf3\x62\x1c\x87\x38\x1e\xf1\x17\x40\x1a\x6e\x78\x52\xd2\xa8\x48\xf3\x30\xca\x93\x7c\x84\x61\x81\x3c\x2c\x92\x32\xcc\x79\x35\x2e\x90\x8f\x8a\x38\x8a\x79\x88\xd9\x4b\x70\x0e\x37\x31\xcf\xca\x3c\x1d\x85\x58\x45\xc8\xc6\x79\xcc\xb2\xac\xcc\x4b\xa2\x71\x5c\x25\x2c\x29\x58\x1e\x8e\xab\x64\x14\x51\x0f\xfc\xf7\x6a\x85\x5d\xe4\x0f\x60\x5a\x92\x96\x58\x2f\x48\xcc\x17\xb6\x87\xd1\xab\x57\xaf\xfa\x9c\x76\x12\x6f\x8e\x2e\xfb\xdf\x01\xdc\x3a\xba\x12\xb2\x6a\x35\xc2\x56\xb5\x30\x77\x3c\x2b\x81\xb4\x56\xda\x01\x64\xb6\x10\x06\x34\x7d\x69\x9d\x15\x61\x40\x2a\x0b\xa6\x6d\x1a\xa5\x2d\x71\x28\x89\x61\x6b\xc8\x49\x6a\x8f\x7f\xb7\x44\xb7\x52\x3a\xae\xf4\x4c\x68\x2c\x5a\xd7\x04\xad\x1b\x1a\xc2\x55\x2b\xbb\xf1\x20\xe8\xc7\x7e\x42\xcd\x16\x62\x45\xc3\x83\x7f\xf4\x4e\x01\xac\x5d\x0f\x59\x05\x5c\xfd\xd3\x4b\x20\xd4\x9e\x85\x1b\xd4\xc2\x6e\x3b\x43\x5e\xcb\xbd\x8f\x87\xe6\x93\xee\xe7\xa7\x7e\x41\x10\xb0\x05\x0a\xf9\x53\x37\x1d\x04\xce\xdb\x9f\x92\x30\x09\x53\x08\x82\x35\xea\xa6\xff\x17\x94\xa8\xb5\x20\x0d\x59\x5e\x84\x61\x18\x42\x10\x48\x15\xa0\x64\x82\xa4\x0d\xca\x5a\xb1\x7b\xd3\x8d\x19\xd2\x2b\x0a\x6a\x97\x54\x08\x82\x25\x6e\x82\xc6\xb5\x29\xc4\x99\x13\x32\x12\x1b\xb3\x50\xb6\x1f\xf4\x63\x4b\x21\x1f\xfd\x74\x3e\x23\xb3\x62\x45\x10\x04\x0e\x9e\x2e\x45\xaa\xaa\x9e\x66\x02\x82\x80\x97\x01\x53\xcb\xc6\xad\x57\x12\x8c\xe1\x2e\x24\x64\x0b\x0a\x8c\xf8\x4a\x90\x86\xe3\x1c\x82\xe0\xb3\x51\x52\x37\x2c\x58\x28\x63\x0d\x60\x5d\x3f\x18\x13\xd2\x92\xae\x90\x91\x1b\xff\xf4\xb8\xdc\x4f\x93\xf9\xbd\xca\x1f\xbb\xf0\x89\xbb\x6e\x92\xd4\x39\x62\x15\xdc\x52\x79\xed\xc6\xad\x01\x9f\x13\x0d\x95\x56\x4b\x68\xa5\xd5\xad\x71\x90\x50\x5a\xcc\x85\x9c\xc0\x70\x78\xf0\x6c\x3d\x5d\xdb\x3e\xa9\xe5\xa7\x20\x68\xa5\xc1\x8a\x02\xda\x34\xca\xd0\x27\xa8\x6a\x9c\x7f\x03\xe0\xff\x8e\xab\xe3\xbf\xc8\xd5\x8f\x7a\xe9\x3f\x66\xeb\x28\x4c\x87\x51\x96\x0e\xa3\x62\x98\x3d\xd9\x9e\x77\x74\x3a\x35\xb9\x40\xba\x69\xdf\xdc\x5d\xb4\xd1\xdb\xcd\xca\x6c\x8f\x67\xd7\x7a\x66\xc6\x2b\x7b\x9c\x97\xf6\xc3\x91\x7c\xf7\x46\x9d\x7f\x2e\xef\xbf\x9e\xe0\xc1\x77\xd4\x67\xc3\xa8\xc8\x86\x71\x32\x7a\xd6\xc0\xc9\x5b\xb6\x16\xb3\xcf\xea\xfd\xed\xbb\xea\x18\xd3\x22\xbe\x99\x5a\xa4\x9b\xcd\xc5\xf9\x9a\x17\x5f\x4b\x79\x1c\x5d\x8f\xd6\x74\x74\x77\xb3\xb9\x7b\x99\xaf\x3d\x69\x3c\xcb\xd6\xf1\xdf\x40\xd7\x2f\xb0\x75\x18\x21\x2f\xf3\x3c\xc1\x8a\x27\x98\xf1\x51\x9e\x65\xac\x18\xf3\x78\x34\x4a\x19\x55\x59\x54\x84\x61\x95\x94\x65\xfe\x22\x5b\x47\x79\x95\x66\x84\x6c\x94\x8c\xb2\x98\xb0\x1c\x25\xe1\x28\xae\x38\x2f\x91\x95\x8e\xa5\x0b\xe4\x2c\xc1\x34\x7c\x99\xad\xa9\x8c\x58\x8e\x49\xce\xd2\x32\x8a\x93\x14\x11\x69\x44\x8c\x8a\xa2\x2a\xd3\x2a\x2d\x78\x9c\xf0\x3c\x89\xd2\xbc\x67\xeb\x2b\xd5\x18\x4b\x4f\xf8\x9a\xab\x79\x83\x96\x2d\xfe\xdc\x69\x24\xf9\x8b\x08\xdf\x59\x87\x1f\x66\xbf\x9c\xfe\x02\x4c\x93\xa3\x6b\xdd\xbb\xea\x50\xee\xf5\xfc\xf8\x2c\xe8\xff\xf6\x43\xca\xff\xef\x98\xd2\x25\xe1\x39\xe0\x27\xff\x5b\xdc\x47\x25\x46\x45\x99\x47\x49\x32\xaa\x30\x8a\xa3\x24\x19\x27\xc9\xb8\xcc\xb2\x74\x94\x84\x2c\xa4\x51\x58\x8e\xb1\x88\xd8\x8b\xb8\xaf\xaa\xac\x4a\xb2\x2a\xaf\x92\x71\x14\x12\xcf\x73\x8c\xd3\x32\xa7\x2c\xcb\xd2\x98\xf2\xbc\x2c\xf2\x22\x8d\x72\x4c\x5e\xc6\x7d\x5a\xb0\x38\xa3\x51\x9e\x8c\xa9\x28\x0a\xca\xf3\x51\x15\x87\x3c\x09\xcb\x71\x9e\x67\x09\xa7\x30\x4b\xe3\x2c\xe2\xc5\xc1\xc0\x5d\xc1\xd0\x22\x5c\x5b\xa5\x71\x4e\x03\xd3\xfd\xef\x2e\x56\x53\xb4\x0b\x9f\x9d\xda\x1d\xdd\x4f\x8f\xa1\x12\x35\x0d\x9c\x51\xbb\x98\xc0\xa1\x5d\x36\x87\xbf\x5f\xf0\xfe\xc5\xd1\xe2\xd0\xaf\xe4\xa5\xd3\x7b\xa2\x64\x25\xe6\xad\xf6\x6e\xed\x0d\x30\x3f\x7a\xfd\xe7\xcd\x74\x0a\x9e\x58\x3b\x62\x4c\xb5\xd2\x1a\xb8\xa7\x2d\xf4\x51\x0c\xb0\x1f\x74\x76\xee\x69\xeb\x86\xa9\xd7\xb8\x9b\x72\xb2\x67\xfb\x9d\x78\xed\x40\xe4\xc1\x70\x34\x3d\x03\x94\x1c\xa6\xf1\x14\xae\xbb\x6d\xd4\xf5\x2d\x49\xd7\x98\x03\xd7\x72\xef\x94\xb1\x12\x97\x34\x81\xd0\x5f\xc9\xc2\xc1\x2b\x98\x2a\x6d\x7b\x25\x4e\xc1\xf7\x05\xdd\xa2\x09\x14\x61\x11\x3b\xe3\xae\x53\x03\xab\xfc\x49\x04\xd8\xc3\x9c\x99\x41\x13\x37\x5d\x8a\xae\x1b\x62\xa2\xda\xc2\xeb\x8d\xf5\x1b\x1e\x9c\x4d\x1f\xf8\xea\x77\x68\x86\xd2\x5d\x70\x35\xb9\x43\x08\x07\xb4\x20\x2a\x28\x69\x21\x24\x87\x8b\xa3\x99\x53\x43\xbd\xf4\xd9\x74\x02\xeb\xe1\x66\xb8\x1d\x7e\xed\x0a\xe0\xbc\x6e\x0d\xf1\x7d\x2b\xb8\xa8\x6b\xdc\x92\x76\x65\xf0\xee\xfa\x46\xf6\xab\x67\x62\x49\xaa\xf5\x61\x4a\x50\x0d\xc9\xfe\xd6\xdd\x1f\x41\x3c\x71\xf9\x63\xd5\x00\x76\xc3\xbd\xc8\x04\x0e\x92\xd0\x78\xd0\x5d\xb6\xd4\xd2\x37\xe1\x7a\xeb\x68\xb6\x92\x2d\xb4\x92\xaa\x35\x8e\x0b\x19\x19\x23\xe4\x7c\xf0\xc5\x09\x74\xc9\xe8\xde\x0c\x4c\x17\x7a\xbb\x2c\x49\x3b\x36\x75\x5d\x4f\xda\x1c\x32\x25\x8d\x23\xe8\x9e\x59\xd7\xee\x16\x57\xfa\x33\x96\x62\x68\xbb\xcc\x18\x8b\xda\xb6\xcd\x00\x9c\xfc\x6d\x27\x38\x81\x2e\xbc\x37\x9a\xc8\x40\xdb\xc0\xc9\xf4\x06\xd8\x96\xd5\x64\xba\x50\x3b\x03\xee\xf8\xbc\x46\xe1\x9f\x1a\x9c\xbf\xb4\x22\x87\x22\xe8\xa7\x6f\x51\xf8\x68\x3f\x5c\x4f\x20\x1a\xf4\xbb\x45\xef\xa1\x26\xab\x05\xf9\x63\xa0\x5a\xf7\xc9\x46\xb0\x68\xdc\x6e\xe1\xfe\x5d\x75\x0b\x26\x10\x85\x2e\x47\x7b\xd2\x33\xbe\xfa\x82\x3d\xce\xd7\x60\x47\x79\x3d\x44\xa8\x26\xc7\x66\xeb\x85\x60\x8b\x3d\x1d\x42\x8f\x73\x57\x14\x77\x0d\xe8\x37\x2c\xe5\xf2\xd7\xef\x34\x1c\x44\x77\xde\x63\xad\xb1\x6a\xd9\x1b\xd9\x35\x61\xff\x2a\xd3\xb7\xd7\x85\xc7\xfb\xc1\x12\x85\x3c\xd8\xbf\xbd\xf8\xfe\xee\x15\xef\xed\xb2\xda\x9d\xd0\x3b\x68\xfe\xb0\x26\x7f\x41\x11\x9a\x60\x6d\x40\x69\x10\x0d\xeb\x1f\x64\xb0\xac\xc9\x7d\x32\xbf\xc7\x75\xd9\x74\x7b\x99\x13\xbc\xb9\x3a\x9f\xc0\xc2\xda\x66\x72\x78\xe8\x4f\xc4\xee\x18\x3d\x19\x67\x69\xb6\xc3\x81\x7f\x30\x9a\xa3\x8b\x45\x30\xe7\xee\x1c\xcd\xd4\x7d\xba\x1c\xee\xfe\x9e\x2c\xae\xc5\x52\xd8\x6e\xf1\xb9\xfb\x9c\x40\x3a\x8a\xe2\xa4\x28\x1e\xe1\xdb\x2a\x5f\xe8\xae\x4c\xf2\xf7\xc8\xac\x46\x69\x70\x7f\xdc\x76\x31\x70\xde\x3d\x30\x21\xf8\x1b\x89\x27\x8e\x2e\x14\xb0\x5a\xcc\xe7\xa4\x89\x77\xdd\x60\x69\x63\x77\x18\xe9\x3a\x22\x0f\x5d\x4b\x3c\x67\x58\x13\x72\x50\xb2\xde\xba\x4e\xdb\xf5\xc9\xee\x95\x6d\xe7\xd2\xef\xaa\xaf\x08\xf9\x63\xf5\x51\xd6\x6b\xbf\x70\x95\x78\xe8\x7b\xa3\x54\x0d\x4b\xdc\xec\x71\x69\x15\x18\x92\xdc\x61\xf2\xc1\x32\xb5\xf2\x2c\xb0\xc4\xcd\x1e\x9e\x71\x9f\xd3\xef\xab\xf4\xf7\x9a\x15\xd6\x5e\xef\xb6\xeb\x1d\x74\x0e\xb2\x56\x6b\xff\xf8\xf3\x40\x62\x81\x06\x4a\x22\x09\x9c\x2c\x31\xeb\xd3\xb4\x53\xe0\xec\xb9\x5d\x31\xee\x23\x38\x15\xc6\xa3\xc5\x6b\x34\x6a\xf9\x04\x6d\x06\xb8\x7a\x78\xfd\x05\xbb\xf1\x1e\x61\x23\x5c\x87\x6d\xa6\x4a\xd5\x47\xcc\x31\xca\x6b\xe9\x34\xf1\x09\x58\xdd\x92\xeb\x35\x94\x5b\xe0\x54\xb6\xf3\x79\xcf\x66\xae\x05\x3c\x77\xcc\x15\x38\x23\x03\x3f\xdb\xb5\x5a\xd3\x68\x55\xf9\xf2\xec\x45\x1c\x4f\xba\xd1\x09\x54\x58\x1b\x1a\x0c\xba\xdd\xbd\x7f\x50\x6c\x34\x31\xb5\xf4\x48\xf3\x06\xff\x1d\x00\x00\xff\xff\xf9\xd1\x4a\xaf\x45\x15\x00\x00") +var _goCentrifugeBuildConfigsDefault_configYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x58\x59\x73\x1b\x37\x12\x7e\xe7\xaf\xe8\xa2\x5f\x92\xaa\x1d\x6a\xee\x83\x55\xa9\x2d\x9d\xb6\x62\x59\xa1\x24\x2a\x8a\xf5\xb2\xc6\x00\x3d\x1c\x58\x43\x60\x0c\x60\x78\xf8\xd7\x6f\x01\x33\x64\x24\xcb\x52\x76\x93\xca\xae\x5e\x34\x04\xd0\x8d\x3e\xbe\xfe\xd0\xc0\x1b\x38\xc1\x8a\x74\x8d\x01\x86\x2b\x6c\x64\xbb\x44\x61\xc0\xa0\x36\x02\x0d\x90\x05\xe1\x42\x1b\x50\x5c\x3c\x60\xb9\x1d\x51\x14\x46\xf1\xaa\x5b\xe0\x25\x9a\xb5\x54\x0f\x53\x50\x9d\xd6\x9c\x88\x9a\x37\xcd\xc8\x29\xe3\x02\xc1\xd4\x08\x6c\xd0\x2b\xfa\x95\x1a\x4c\x4d\x0c\x1c\xef\x35\xc0\x92\x70\x61\xac\xfe\xd1\x6e\xc9\x74\x04\xf0\x06\x2e\x24\x25\x8d\x33\x81\x8b\x05\x50\x29\x8c\x22\xd4\x00\x61\x4c\xa1\xd6\xa8\x41\x20\x32\x30\x12\x4a\x04\x8d\x06\xd6\xdc\xd4\x80\x62\x05\x2b\xa2\x38\x29\x1b\xd4\x93\x11\xec\xe4\xad\x4a\x00\xce\xa6\x10\x45\x91\xfb\x46\x53\xa3\xc2\x6e\x39\x78\x70\xce\xa6\x90\x47\x79\x3f\x57\x4a\x69\xb4\x51\xa4\x9d\x21\x2a\xdd\xcb\x7a\x30\x3e\xe0\x6d\x7c\x10\x84\xd9\xc4\x9f\xf8\x93\xe0\xc0\xd0\xf6\x20\xca\x43\x3f\x3c\xe0\x6d\xa5\x0f\xae\x96\xf3\xab\x4d\xb9\x7e\xe8\xee\x3f\x7e\x3c\xa9\xba\xaf\xf3\x72\x73\x7a\x78\x8d\xf3\xcb\xe3\x0b\xf9\x75\xbb\x4d\x92\x7c\x75\x25\x16\xbf\xae\x66\x1f\x3e\x5f\x7c\x7c\x18\xff\x81\xd2\x68\xa7\xf4\xd7\x2a\x3d\xbd\x4c\x97\x0f\x5f\xee\xf0\xf3\xdd\xfb\xbb\xf0\xcb\xac\x0b\xd2\xdf\x5a\xf6\x36\x7a\xf8\x59\x06\xf3\x68\x59\x93\x7a\x76\x94\xdc\x60\x22\x82\x5e\xe9\x2e\x54\x87\xbb\x48\xf5\x0e\x58\xf7\x51\x18\x6e\xb6\x67\x84\x1a\xa9\xb6\x53\x18\x8f\x87\x19\x22\x68\x2d\xd5\x35\xb6\x52\xf3\x6f\xa6\x5a\xb2\xb5\x58\xf8\xa5\x6c\xf8\x82\x18\x2e\x85\x9b\x73\x19\xfa\x40\xb8\xf8\x2e\x5e\x86\x44\xc2\x0f\xd7\x3d\x60\x7e\x1c\xc1\x63\x80\xf4\xf6\xbc\x81\xcb\x6e\x89\x8a\x53\x38\x3f\x01\x59\x39\xb0\x3c\x82\xc5\xa0\x63\x9f\xb7\x24\x18\xa4\x8e\x76\xc9\x81\x86\x6b\x63\x25\x85\x64\xf8\x1c\x57\xad\x92\x2b\xee\x26\xa4\xd3\xfd\xc8\x80\x9d\xa1\x7f\x98\xec\x28\x99\x84\x61\x32\x09\x7d\x7f\x12\x87\xdf\x26\x3c\x08\x4f\xa2\xf7\x52\xde\x5d\x70\x4e\xaf\x7e\x5d\xcf\xeb\xf9\xd1\xc7\x74\xf3\x9e\xce\xe4\x45\x95\x5e\x5f\x7d\xfc\xf9\xac\x5d\x57\x81\xca\x92\xf5\xc5\x26\xbc\xbf\x8e\xda\x63\x16\x8c\xbf\xa7\x3e\x4f\x27\x61\xe0\xbf\xa4\xfe\xea\xfe\xc3\x61\xfe\x76\xf6\x4e\xad\x4e\xef\x8f\x8a\x35\x7b\x90\xb7\xf4\xf0\x70\x79\x7c\xff\xae\x2d\x70\xbb\xbd\x8f\x6f\x4e\xf3\xc5\x99\x8a\xea\xf9\xe5\x6f\xe3\x21\x46\xa7\x03\xb8\xf7\x99\x38\x3f\x01\x0f\x86\x6c\xbc\x04\xff\x78\x10\xbe\x20\x36\x3c\xc0\xb0\x6d\xe4\x16\x19\xdc\x2c\x89\x32\x70\x3c\xa0\x4a\x43\x25\x95\x0b\xe8\x82\xaf\x50\x3c\x09\xe5\x73\xe4\xc1\x8b\xd0\xf3\x37\x65\xe8\x57\x09\x32\xdf\xcf\x8a\x98\xfa\x94\x52\x9a\xf8\x79\x19\xb0\xa2\x22\x79\x1e\x96\x69\x14\x90\xa8\xaa\xd2\xe0\x15\x90\xfa\x9b\x30\xf4\x7d\x96\xd3\x22\x08\x93\x24\xa0\x94\xd1\xaa\x48\x7d\x16\xf9\x61\x15\x05\x39\x8b\x90\x62\xca\xa2\x22\x29\x5e\x83\xb3\xbf\xf1\x03\x42\xa3\xa0\x08\xca\x2c\x0d\x31\xf1\xb3\x90\xd2\x30\xc1\x2a\xa1\x04\x19\x06\x09\x09\xb2\x3c\xf6\x49\x5e\x0c\xc0\x7f\x2f\x57\xa4\xf7\xfc\x11\x4c\x4b\x54\x82\x34\x35\xf2\x45\x6d\x06\x18\xbd\x79\xf3\x66\x88\x69\x2f\x71\x76\x78\x35\xfc\xf6\xe0\xce\xd2\x15\x17\x55\xa7\x08\x6c\x65\x07\x0b\xcb\xb3\x02\x50\x29\xa9\x2c\x40\xe6\x35\xd7\xa0\xf0\x4b\x67\x77\xe1\x1a\x84\x34\xa0\xbb\xb6\x95\xca\x20\x83\x12\x29\xe9\x34\x5a\x49\xe5\xf0\x6f\x97\xa8\x4e\x08\xcb\x95\x8e\x09\xb5\x21\xc6\x16\x41\x67\x87\x26\x70\xdd\x89\x7e\xdc\xf3\x86\xb1\x9f\x88\xa2\x35\x5f\xe1\x64\xfc\x8f\xc1\x28\x80\xb5\xad\x21\x23\x81\xc9\x7f\x3a\x09\x02\x8d\x63\xe1\x96\x28\x6e\xb6\xfd\x46\x4e\xcb\x83\xf3\x07\x17\xd3\xfe\xe7\xa7\x61\x81\xe7\xd1\x9a\x70\xf1\x53\x3f\xed\x79\xd6\xda\x9f\x22\x3f\xf2\x63\xf0\xbc\x35\x51\xed\xf0\xcf\x2b\x89\x52\x1c\x15\x24\x69\xee\xfb\xbe\x0f\x9e\x27\xa4\x47\x04\xe5\x28\x8c\x57\x36\x92\x3e\xe8\x7e\x4c\xa3\x5a\xa1\xd7\xd8\xa0\x82\xe7\x2d\xc9\xc6\x6b\x6d\x99\x42\x98\x58\x21\x2d\x48\xab\x6b\x69\x86\x41\x37\xb6\xe4\xe2\xc9\x4f\x6b\x33\xa1\x86\xaf\x10\x3c\xcf\xc2\xd3\x86\x48\x56\xd5\xf3\x48\x80\xe7\xb1\xd2\xa3\x72\xd9\xda\xf5\x52\x80\xd6\xcc\xba\x44\x68\x8d\x9e\xe6\x5f\x11\x62\xbf\x48\xc1\xf3\x3e\x6b\x29\x54\x4b\xbd\x5a\x6a\xa3\x81\x34\xcd\xa3\x31\x2e\x0c\xaa\x8a\x50\xb4\xe3\x9f\x9e\xa6\xfb\x79\x30\xbf\x97\xf9\x23\xeb\x3e\x32\x5b\x4d\x02\x7b\x43\x8c\x84\x3b\x2c\x6f\xec\xb8\xd1\xe0\x62\xa2\xa0\x52\x72\x09\x9d\x30\xaa\xd3\x16\x12\x52\xf1\x05\x17\x53\x98\x4c\xc6\x2f\xe6\xd3\x96\xed\xb3\x5c\x7e\xf2\xbc\x4e\x68\x52\xa1\x87\x9b\x56\x6a\xfc\x04\x55\x43\x16\xdf\x00\xf8\xbf\xe3\xea\xf0\x2f\x72\xf5\x93\x5a\xfa\x8f\xd9\x3a\xf0\xe3\x49\x90\xc4\x93\x20\x9f\x24\xcf\x8e\xe7\x1d\x9d\xce\x74\xca\x09\xde\x76\x67\xf7\x97\x5d\xf0\x76\xb3\xd2\xdb\xa3\xf9\x8d\x9a\xeb\x62\x65\x8e\xd2\xd2\x7c\x38\x14\xef\xce\xe4\xc5\xe7\xf2\xe1\xeb\x31\x19\x7f\x47\x7d\x32\x09\xf2\x64\x12\x46\xd9\x8b\x1b\x1c\xbf\xa5\x6b\x3e\xff\x2c\xdf\xdf\xbd\xab\x8e\x48\x9c\x87\xb7\x33\x43\xf0\x76\x73\x79\xb1\x66\xf9\xd7\x52\x1c\x05\x37\xd9\x1a\x0f\xef\x6f\x37\xf7\xaf\xf3\xb5\x23\x8d\x17\xd9\x3a\xfc\x1b\xe8\xfa\x15\xb6\x8e\x69\x1e\xfb\x45\xe1\xd3\x04\x8b\xb4\x8a\x69\x1c\x27\x79\x9c\xa7\x2c\x8e\x69\x9a\x23\xcb\xb0\x48\xd0\x67\x49\xf8\x2a\x5b\xa7\x61\x52\x16\x09\x8b\x33\x3f\x61\x59\x42\xe3\x3c\x61\x41\x96\x45\x34\x0b\xfd\x80\x64\x51\x1c\xa5\x71\x84\x41\x50\xbd\xce\xd6\x79\x55\x86\x58\x95\x59\x56\x86\x2c\x67\x7e\x41\xb2\x22\x2a\x59\x14\x44\x58\xd2\x3c\xf2\x49\x86\x99\x5f\xf8\x65\x36\xb0\xf5\xb5\x6c\xb5\xc1\x67\x7c\xcd\xe4\xa2\x25\x86\xd6\x7f\xae\x1b\x89\xfe\x22\xc2\x77\xbb\xc3\x0f\xf3\x5f\x4e\x7e\x01\xaa\xd0\xd2\xb5\x1a\x4c\xb5\x28\x77\x7a\x7e\x7c\x11\xf4\x7f\x7b\x93\xf2\xff\x6b\x53\xfa\x20\xbc\x04\xfc\xe8\x7f\x8b\xfb\xa0\x24\x41\x5e\xa6\x41\x14\x65\x15\x09\xc2\x20\x8a\x8a\x28\x2a\xca\x24\x89\xb3\xc8\xa7\x3e\x66\x7e\x59\x90\x3c\xa0\xaf\xe2\xbe\xaa\x92\x2a\x4a\xaa\xb4\x8a\x8a\xc0\x47\x96\xa6\x24\x8c\xcb\x14\x93\x24\x89\x43\x4c\xd3\x32\x4f\xf3\x38\x48\x49\xf4\x3a\xee\xe3\xdc\x76\x25\x59\x1a\x15\x98\xe7\x39\xa6\x69\x56\x85\xb6\xd7\x29\x8b\x34\x4d\x22\x86\x7e\x12\x87\x49\xc0\xf2\xf1\xc8\x5e\xc1\x88\x21\x70\x63\xa4\x22\x0b\x1c\xe9\xfe\x7f\x7f\xb1\x9a\x11\x53\xbb\xe8\x34\xb6\x75\x3f\x39\x82\x8a\x37\x38\xb2\x9b\x9a\x7a\x0a\x07\x66\xd9\x1e\xfc\x7e\xc1\xfb\x17\x23\x86\x4c\xdc\x4a\x56\x5a\xbd\xc7\x52\x54\x7c\xd1\x29\x67\xd6\x7e\x03\xea\x46\x6f\xfe\xfc\x36\xbd\x82\x67\xbb\x1d\x52\x2a\x3b\x61\x34\x3c\xe0\x16\x06\x2f\x46\x64\x18\xb4\xfb\x3c\xe0\xd6\x0e\xe3\xa0\x71\x37\x65\x65\xcf\xf7\x27\xf1\xda\x82\xc8\x81\xe1\x70\x76\x0e\x44\x30\x98\x85\x33\xb8\xe9\x8f\x51\x5b\xb7\x28\x6c\x61\x8e\x6c\xc9\xbd\x93\xda\x08\xb2\xc4\x29\xf8\xee\x4a\xe6\x8f\xde\xc0\x4c\x2a\x33\x28\xb1\x0a\xbe\x2f\x68\x17\x4d\x21\xf7\xf3\xd0\x6e\x6e\x2b\xd5\x33\xd2\x75\x22\x40\x1f\xc7\x4c\x8f\xda\xb0\xed\x43\x74\xd3\x22\xe5\xd5\x16\x4e\x37\xc6\x1d\x78\x70\x3e\x7b\x64\xab\x3b\xa1\x29\x11\xf6\x82\xab\xd0\x36\x21\x0c\x88\x01\x5e\x41\x89\x35\x17\x0c\x2e\x0f\xe7\x56\x0d\x0e\xd2\xe7\xb3\x29\xac\x27\x9b\xc9\x76\xf2\xb5\x4f\x80\xb5\xba\xd3\xc8\xf6\xa5\x60\xbd\x6e\xc8\x16\x95\x4d\x83\x33\xd7\x15\xb2\x5b\x3d\xe7\x4b\x94\x9d\x73\x53\x80\x6c\x51\x0c\xb7\xee\xa1\x05\x71\xc4\xe5\xda\xaa\x11\xec\x86\x07\x91\x29\x8c\x23\x5f\x3b\xd0\x5d\x75\xd8\xe1\x37\xee\xba\xdd\x89\xde\x0a\x5a\x2b\x29\x64\xa7\x2d\x17\x52\xd4\x9a\x8b\xc5\xe8\x8b\x15\xe8\x83\xd1\xbf\x19\xe8\xde\xf5\x6e\x59\xa2\xb2\x6c\x6a\xab\x1e\x95\x3e\xa0\x52\x68\x4b\xd0\x03\xb3\xae\xed\x2d\xae\x74\x3d\x96\xa4\xc4\xf4\x91\xd1\x86\x28\xd3\xb5\x23\xb0\xf2\x77\xbd\xe0\x14\x7a\xf7\xce\x14\xa2\x86\xae\x85\xe3\xd9\x2d\xd0\x2d\x6d\x50\xf7\xae\xf6\x1b\xd8\xf6\x79\x4d\xb8\x7b\x6a\xb0\xf6\xe2\x0a\x2d\x8a\x60\x98\xbe\x23\xdc\x79\xfb\xe1\x66\x0a\xc1\x68\x38\x2d\x06\x0b\x15\x1a\xc5\xd1\xb5\x81\x72\x3d\x04\x9b\x80\x21\xda\x9e\x16\xf6\xdf\x75\xbf\x60\x0a\x81\x6f\x63\xb4\x27\x3d\xed\xb2\xcf\xe9\xd3\x78\x8d\x76\x94\x37\x40\x04\x1b\xb4\x6c\xb6\xae\x39\xad\xf7\x74\x08\x03\xce\x6d\x52\xec\x35\x60\x38\xb0\xa4\x8d\xdf\x70\xd2\x30\xe0\x7d\xbf\x47\x3b\x6d\xe4\x72\xd8\x64\x57\x84\xc3\xab\xcc\x50\x5e\x97\x0e\xef\xe3\x25\xe1\x62\xbc\x7f\x7b\x71\xf5\x3d\x28\xde\xef\x4b\x1b\xdb\xa1\xf7\xd0\xfc\x61\x8d\xee\x82\xc2\x15\xc2\x5a\x83\x54\xc0\x5b\x3a\x3c\xc8\x90\xb2\x41\xfb\x49\xdd\x19\xd7\x47\xd3\x9e\x65\x56\xf0\xf6\xfa\x62\x0a\xb5\x31\xed\xf4\xe0\xc0\x75\xc4\xb6\x8d\x9e\x16\x49\x9c\xec\x70\xe0\x1e\x8c\x16\xc4\xfa\xc2\xa9\x35\x77\x41\xf4\xcc\x7e\xda\x18\xee\xfe\x9e\x2d\x6e\xf8\x92\x9b\x7e\xf1\x85\xfd\x9c\x42\x9c\x05\x61\x94\xe7\x4f\xf0\x6d\xa4\x4b\x74\x9f\x26\xf1\xbb\x67\x46\x11\xa1\xc9\xbe\xdd\xb6\x3e\x30\xd6\x3f\x30\x11\x70\x37\x12\x47\x1c\xbd\x2b\x60\x14\x5f\x2c\x50\x21\xeb\xab\xc1\xe0\xc6\xec\x30\xd2\x57\x44\xea\xdb\x92\x78\x69\x63\x85\x84\x81\x14\xcd\xd6\x56\xda\xae\x4e\x76\xaf\x6c\x3b\x93\x7e\x57\x7d\x8d\x84\x3d\x55\x1f\x24\x83\xf6\x4b\x9b\x89\xc7\xb6\xb7\x52\x36\xb0\x24\x9b\x3d\x2e\x8d\x04\x8d\x82\x59\x4c\x3e\x5a\x26\x57\x8e\x05\x96\x64\xb3\x87\x67\x38\xc4\xf4\xfb\x2a\xdd\xbd\x66\x45\x1a\xa7\x77\xdb\xd7\x0e\xb1\x06\xd2\x4e\x29\xf7\xf8\xf3\x48\xa2\x26\x1a\x4a\x44\x01\x0c\x0d\x52\xe3\xc2\xb4\x53\x60\xf7\xb3\xa7\x62\x38\x78\x70\xc2\xb5\x43\x8b\xd3\xa8\xe5\xf2\x19\xda\x34\x30\xf9\xf8\xfa\x0b\x66\xe3\x2c\x22\x2d\xb7\x15\xb6\x99\x49\xd9\x1c\x52\xcb\x28\xa7\xc2\x6a\x62\x53\x30\xaa\x43\x5b\x6b\x44\x6c\x81\x61\xd9\x2d\x16\x03\x9b\xd9\x12\x70\xdc\xb1\x90\x60\x37\x19\xb9\xd9\xbe\xd4\xda\x56\xc9\xca\xa5\x67\x2f\x62\x79\xd2\x8e\x4e\xa1\x22\x8d\xc6\xd1\xa8\x3f\xdd\x87\x07\xc5\x56\x21\x95\x4b\x87\x34\xb7\xe1\xbf\x03\x00\x00\xff\xff\xd3\xea\x79\x07\x45\x15\x00\x00") func goCentrifugeBuildConfigsDefault_configYamlBytes() ([]byte, error) { return bindataRead( @@ -84,7 +84,7 @@ func goCentrifugeBuildConfigsDefault_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "go-centrifuge/build/configs/default_config.yaml", size: 5445, mode: os.FileMode(420), modTime: time.Unix(1551950596, 0)} + info := bindataFileInfo{name: "go-centrifuge/build/configs/default_config.yaml", size: 5445, mode: os.FileMode(420), modTime: time.Unix(1552665458, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -104,7 +104,7 @@ func goCentrifugeBuildConfigsTesting_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "go-centrifuge/build/configs/testing_config.yaml", size: 1076, mode: os.FileMode(420), modTime: time.Unix(1551953677, 0)} + info := bindataFileInfo{name: "go-centrifuge/build/configs/testing_config.yaml", size: 1076, mode: os.FileMode(420), modTime: time.Unix(1552303474, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -204,7 +204,6 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "go-centrifuge": &bintree{nil, map[string]*bintree{ "build": &bintree{nil, map[string]*bintree{ @@ -262,3 +261,4 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/testingutils/anchors/mock_anchor_repo.go b/testingutils/anchors/mock_anchor_repo.go index 46e376f85..51d530ab0 100644 --- a/testingutils/anchors/mock_anchor_repo.go +++ b/testingutils/anchors/mock_anchor_repo.go @@ -3,6 +3,8 @@ package testinganchors import ( + "time" + "github.com/centrifuge/go-centrifuge/anchors" "github.com/stretchr/testify/mock" ) @@ -12,8 +14,8 @@ type MockAnchorRepo struct { anchors.AnchorRepository } -func (r *MockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { +func (r *MockAnchorRepo) GetAnchorData(anchorID anchors.AnchorID) (docRoot anchors.DocumentRoot, anchoredTime time.Time, err error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocumentRoot) - return docRoot, args.Error(1) + docRoot, _ = args.Get(0).(anchors.DocumentRoot) + return docRoot, anchoredTime, args.Error(1) } diff --git a/testingutils/commons/mock_did_identity.go b/testingutils/commons/mock_did_identity.go index 474e89fc4..75326ca53 100644 --- a/testingutils/commons/mock_did_identity.go +++ b/testingutils/commons/mock_did_identity.go @@ -4,8 +4,8 @@ package testingcommons import ( "context" + "time" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/identity" "github.com/ethereum/go-ethereum/common" @@ -39,21 +39,15 @@ func (i *MockIdentityService) GetKey(did identity.DID, key [32]byte) (*identity. } // RawExecute calls the execute method on the identity contract -func (i *MockIdentityService) RawExecute(ctx context.Context, to common.Address, data []byte) error { +func (i *MockIdentityService) RawExecute(ctx context.Context, to common.Address, data []byte) (txID identity.IDTX, done chan bool, err error) { args := i.Called(ctx, to, data) - return args.Error(0) + return args.Get(0).(identity.IDTX), args.Get(1).(chan bool), args.Error(2) } // Execute creates the abi encoding an calls the execute method on the identity contract -func (i *MockIdentityService) Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) error { +func (i *MockIdentityService) Execute(ctx context.Context, to common.Address, contractAbi, methodName string, args ...interface{}) (txID identity.IDTX, done chan bool, err error) { a := i.Called(ctx, to, contractAbi, methodName, args) - return a.Error(0) -} - -// IsSignedWithPurpose verifies if a message is signed with one of the identities specific purpose keys -func (i *MockIdentityService) IsSignedWithPurpose(did identity.DID, message [32]byte, signature []byte, purpose *big.Int) (bool, error) { - args := i.Called(did, message, signature, purpose) - return args.Get(0).(bool), args.Error(1) + return a.Get(0).(identity.IDTX), a.Get(1).(chan bool), a.Error(2) } // AddMultiPurposeKey adds a key with multiple purposes @@ -83,14 +77,14 @@ func (i *MockIdentityService) Exists(ctx context.Context, did identity.DID) erro } // ValidateKey checks if a given key is valid for the given centrifugeID. -func (i *MockIdentityService) ValidateKey(ctx context.Context, did identity.DID, key []byte, purpose *big.Int) error { +func (i *MockIdentityService) ValidateKey(ctx context.Context, did identity.DID, key []byte, purpose *big.Int, at *time.Time) error { args := i.Called(ctx, did, key, purpose) return args.Error(0) } // ValidateSignature checks if signature is valid for given identity -func (i *MockIdentityService) ValidateSignature(signature *coredocumentpb.Signature, message []byte) error { - args := i.Called(signature, message) +func (i *MockIdentityService) ValidateSignature(did identity.DID, pubKey []byte, signature []byte, message []byte, timestamp time.Time) error { + args := i.Called(did, pubKey, signature, message, timestamp) return args.Error(0) } @@ -109,9 +103,9 @@ func (i *MockIdentityService) GetClientsP2PURLs(dids []*identity.DID) ([]string, } // GetKeysByPurpose returns keys grouped by purpose from the identity contract. -func (i *MockIdentityService) GetKeysByPurpose(did identity.DID, purpose *big.Int) ([][32]byte, error) { +func (i *MockIdentityService) GetKeysByPurpose(did identity.DID, purpose *big.Int) ([]identity.KeyDID, error) { args := i.Called(did, purpose) - return args.Get(0).([][32]byte), args.Error(1) + return args.Get(0).([]identity.KeyDID), args.Error(1) } // MockIdentityFactory implements Service @@ -128,3 +122,8 @@ func (s *MockIdentityFactory) CalculateIdentityAddress(ctx context.Context) (*co args := s.Called(ctx) return args.Get(0).(*common.Address), args.Error(1) } + +func (s *MockIdentityFactory) IdentityExists(did *identity.DID) (exists bool, err error) { + args := s.Called(did) + return args.Get(0).(bool), args.Error(1) +} diff --git a/testingutils/config/config.go b/testingutils/config/config.go index b7155ddda..f47053a85 100644 --- a/testingutils/config/config.go +++ b/testingutils/config/config.go @@ -1,4 +1,4 @@ -// +build unit integration +// +build unit integration testworld package testingconfig diff --git a/testingutils/documents/documents.go b/testingutils/documents/documents.go index 34847f498..e7a7bf4ab 100644 --- a/testingutils/documents/documents.go +++ b/testingutils/documents/documents.go @@ -1,4 +1,4 @@ -// +build unit integration +// +build unit integration testworld package testingdocuments @@ -7,6 +7,7 @@ import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/identity" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/mock" ) @@ -41,12 +42,12 @@ func (m *MockService) DeriveFromCoreDocument(cd coredocumentpb.CoreDocument) (do return args.Get(0).(documents.Model), args.Error(1) } -func (m *MockService) RequestDocumentSignature(ctx context.Context, model documents.Model) (*coredocumentpb.Signature, error) { +func (m *MockService) RequestDocumentSignature(ctx context.Context, model documents.Model, collaborator identity.DID) (*coredocumentpb.Signature, error) { args := m.Called() return args.Get(0).(*coredocumentpb.Signature), args.Error(1) } -func (m *MockService) ReceiveAnchoredDocument(ctx context.Context, model documents.Model, senderID []byte) error { +func (m *MockService) ReceiveAnchoredDocument(ctx context.Context, model documents.Model, collaborator identity.DID) error { args := m.Called() return args.Error(0) } @@ -61,6 +62,11 @@ type MockModel struct { mock.Mock } +func (m *MockModel) PreviousVersion() []byte { + args := m.Called() + return args.Get(0).([]byte) +} + func (m *MockModel) CurrentVersion() []byte { args := m.Called() return args.Get(0).([]byte) diff --git a/testingutils/documents/invoice.go b/testingutils/documents/invoice.go index 6f768be50..902d6d982 100644 --- a/testingutils/documents/invoice.go +++ b/testingutils/documents/invoice.go @@ -1,4 +1,4 @@ -// +build integration unit +// +build integration unit testworld package testingdocuments diff --git a/testingutils/documents/purchaseorder.go b/testingutils/documents/purchaseorder.go index b502e3836..93458a0dd 100644 --- a/testingutils/documents/purchaseorder.go +++ b/testingutils/documents/purchaseorder.go @@ -1,4 +1,4 @@ -// +build integration unit +// +build integration unit testworld package testingdocuments diff --git a/testingutils/identity/identity.go b/testingutils/identity/identity.go index 9586b7b14..24c92efd0 100644 --- a/testingutils/identity/identity.go +++ b/testingutils/identity/identity.go @@ -1,4 +1,4 @@ -// +build integration unit +// +build integration unit testworld package testingidentity @@ -9,9 +9,8 @@ import ( "github.com/centrifuge/go-centrifuge/config/configstore" "github.com/centrifuge/go-centrifuge/contextutil" - "github.com/centrifuge/go-centrifuge/utils" - "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/utils" ) func CreateAccountIDWithKeys(contextTimeout time.Duration, acc *configstore.Account, idService identity.ServiceDID, idFactory identity.Factory) (identity.DID, error) { @@ -32,19 +31,28 @@ func CreateAccountIDWithKeys(contextTimeout time.Duration, acc *configstore.Acco err = idService.Exists(ctxh, *did) } - // only add key if it doesn't exist - ctx, cancel := defaultWaitForTransactionMiningContext(contextTimeout) - ctxh, err = contextutil.New(ctx, acc) - if err != nil { - return identity.DID{}, nil + // Add Action key if it doesn't exist + keys, err := idService.GetKeysByPurpose(*did, &(identity.KeyPurposeAction.Value)) + ctx, cancel1 := defaultWaitForTransactionMiningContext(contextTimeout) + ctxh, _ = contextutil.New(ctx, acc) + defer cancel1() + if err != nil || len(keys) == 0 { + pk, _ := utils.SliceToByte32(idKeys[identity.KeyPurposeAction.Name].PublicKey) + keyDID := identity.NewKey(pk, &(identity.KeyPurposeAction.Value), big.NewInt(identity.KeyTypeECDSA), 0) + err = idService.AddKey(ctxh, keyDID) + if err != nil { + return identity.DID{}, nil + } } - keys, err := idService.GetKeysByPurpose(*did, &(identity.KeyPurposeSigning.Value)) - ctx, cancel = defaultWaitForTransactionMiningContext(contextTimeout) + + // Add Signing key if it doesn't exist + keys, err = idService.GetKeysByPurpose(*did, &(identity.KeyPurposeSigning.Value)) + ctx, cancel2 := defaultWaitForTransactionMiningContext(contextTimeout) ctxh, _ = contextutil.New(ctx, acc) - defer cancel() + defer cancel2() if err != nil || len(keys) == 0 { pk, _ := utils.SliceToByte32(idKeys[identity.KeyPurposeSigning.Name].PublicKey) - keyDID := identity.NewKey(pk, &(identity.KeyPurposeSigning.Value), big.NewInt(identity.KeyTypeECDSA)) + keyDID := identity.NewKey(pk, &(identity.KeyPurposeSigning.Value), big.NewInt(identity.KeyTypeECDSA), 0) err = idService.AddKey(ctxh, keyDID) if err != nil { return identity.DID{}, nil diff --git a/testingutils/setup.go b/testingutils/setup.go index db0225a53..5f71744b5 100644 --- a/testingutils/setup.go +++ b/testingutils/setup.go @@ -41,17 +41,20 @@ func RunSmartContractMigrations() { } var err error + var out []byte projDir := GetProjectDir() migrationScript := path.Join(projDir, "build", "scripts", "migrate.sh") for i := 0; i < 3; i++ { - log.Infof("Trying to migrate contracts for the %d th time", i) - _, err = exec.Command(migrationScript, projDir).Output() + fmt.Printf("Trying to migrate contracts for the %d th time\n", i) + out, err = exec.Command(migrationScript, projDir).CombinedOutput() + fmt.Println(string(out)) if err == nil { return } } + // trying 3 times to migrate didnt work - log.Fatal(err) + log.Fatal(err, string(out)) } // GetSmartContractAddresses finds migrated smart contract addresses for localgeth diff --git a/testingutils/testingtx/mocktx.go b/testingutils/testingtx/mocktx.go index a0706fa69..458cb562a 100644 --- a/testingutils/testingtx/mocktx.go +++ b/testingutils/testingtx/mocktx.go @@ -20,6 +20,10 @@ func (m MockTxManager) GetDefaultTaskTimeout() time.Duration { panic("implement me") } +func (m MockTxManager) UpdateTransactionWithValue(accountID identity.DID, id transactions.TxID, key string, value []byte) error { + panic("implement me") +} + func (m MockTxManager) UpdateTaskStatus(accountID identity.DID, id transactions.TxID, status transactions.Status, taskName, message string) error { panic("implement me") } diff --git a/testworld/document_consensus_test.go b/testworld/document_consensus_test.go index cf75aa57b..00a77c4aa 100644 --- a/testworld/document_consensus_test.go +++ b/testworld/document_consensus_test.go @@ -59,7 +59,10 @@ func addExternalCollaborator_withinHost(t *testing.T, documentType string) { // a shares document with b first res := createDocument(bob.httpExpect, a, documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{b})) txID := getTransactionID(t, res) - waitTillStatus(t, bob.httpExpect, a, txID, "success") + status, message := getTransactionStatusAndMessage(bob.httpExpect, a, txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -74,10 +77,17 @@ func addExternalCollaborator_withinHost(t *testing.T, documentType string) { getDocumentAndCheck(bob.httpExpect, b, documentType, params) nonExistingDocumentCheck(bob.httpExpect, c, documentType, params) + //// let c update the document and fail + //res = failedUpdateDocument(bob.httpExpect, c, documentType, http.StatusInternalServerError, docIdentifier, updatedDocumentPayload(documentType, []string{a, c})) + //assert.NotNil(t, res) + // b updates invoice and shares with c as well res = updateDocument(bob.httpExpect, b, documentType, http.StatusOK, docIdentifier, updatedDocumentPayload(documentType, []string{a, c})) txID = getTransactionID(t, res) - waitTillStatus(t, bob.httpExpect, b, txID, "success") + status, message = getTransactionStatusAndMessage(bob.httpExpect, b, txID) + if status != "success" { + t.Error(message) + } docIdentifier = getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -105,7 +115,10 @@ func addExternalCollaborator_multiHostMultiAccount(t *testing.T, documentType st // Alice shares document with Bobs accounts a and b res := createDocument(alice.httpExpect, alice.id.String(), documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{a, b})) txID := getTransactionID(t, res) - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -121,10 +134,17 @@ func addExternalCollaborator_multiHostMultiAccount(t *testing.T, documentType st getDocumentAndCheck(bob.httpExpect, b, documentType, params) nonExistingDocumentCheck(bob.httpExpect, c, documentType, params) + //// let c update the document and fail + //res = failedUpdateDocument(bob.httpExpect, c, documentType, http.StatusInternalServerError, docIdentifier, updatedDocumentPayload(documentType, []string{alice.id.String(), b, c, d, e})) + //assert.NotNil(t, res) + // Bob updates invoice and shares with bobs account c as well using account a and to accounts d and e of Charlie res = updateDocument(bob.httpExpect, a, documentType, http.StatusOK, docIdentifier, updatedDocumentPayload(documentType, []string{alice.id.String(), b, c, d, e})) txID = getTransactionID(t, res) - waitTillStatus(t, bob.httpExpect, a, txID, "success") + status, message = getTransactionStatusAndMessage(bob.httpExpect, a, txID) + if status != "success" { + t.Error(message) + } docIdentifier = getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -149,7 +169,10 @@ func addExternalCollaborator(t *testing.T, documentType string) { // Alice shares document with Bob first res := createDocument(alice.httpExpect, alice.id.String(), documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{bob.id.String()})) txID := getTransactionID(t, res) - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -164,10 +187,17 @@ func addExternalCollaborator(t *testing.T, documentType string) { getDocumentAndCheck(bob.httpExpect, bob.id.String(), documentType, params) nonExistingDocumentCheck(charlie.httpExpect, charlie.id.String(), documentType, params) + //// let charlie update the document and fail + //res = failedUpdateDocument(charlie.httpExpect, charlie.id.String(), documentType, http.StatusInternalServerError, docIdentifier, updatedDocumentPayload(documentType, []string{alice.id.String(), charlie.id.String()})) + //assert.NotNil(t, res) + // Bob updates invoice and shares with Charlie as well res = updateDocument(bob.httpExpect, bob.id.String(), documentType, http.StatusOK, docIdentifier, updatedDocumentPayload(documentType, []string{alice.id.String(), charlie.id.String()})) txID = getTransactionID(t, res) - waitTillStatus(t, bob.httpExpect, bob.id.String(), txID, "success") + status, message = getTransactionStatusAndMessage(bob.httpExpect, bob.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier = getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -190,14 +220,16 @@ func TestHost_CollaboratorTimeOut(t *testing.T) { } func collaboratorTimeOut(t *testing.T, documentType string) { - kenny := doctorFord.getHostTestSuite(t, "Kenny") bob := doctorFord.getHostTestSuite(t, "Bob") // Kenny shares a document with Bob response := createDocument(kenny.httpExpect, kenny.id.String(), documentType, http.StatusOK, defaultInvoicePayload([]string{bob.id.String()})) txID := getTransactionID(t, response) - waitTillStatus(t, kenny.httpExpect, kenny.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(kenny.httpExpect, kenny.id.String(), txID) + if status != "success" { + t.Error(message) + } // check if Bob and Kenny received the document docIdentifier := getDocumentIdentifier(t, response) @@ -217,7 +249,10 @@ func collaboratorTimeOut(t *testing.T, documentType string) { // Bob will anchor the document without Kennys signature response = updateDocument(bob.httpExpect, bob.id.String(), documentType, http.StatusOK, docIdentifier, updatedPayload) txID = getTransactionID(t, response) - waitTillStatus(t, bob.httpExpect, bob.id.String(), txID, "failed") + status, message = getTransactionStatusAndMessage(bob.httpExpect, bob.id.String(), txID) + if status != "failed" { + t.Error(message) + } // check if bob saved the updated document paramsV2 := map[string]interface{}{ diff --git a/testworld/httputils.go b/testworld/httputils.go index bb880ed42..0c549c350 100644 --- a/testworld/httputils.go +++ b/testworld/httputils.go @@ -5,6 +5,7 @@ package testworld import ( "crypto/tls" "net/http" + "os" "testing" "time" @@ -15,13 +16,25 @@ const typeInvoice string = "invoice" const typePO string = "purchaseorder" const poPrefix string = "po" +var isRunningOnCI = len(os.Getenv("TRAVIS")) != 0 + +type httpLog struct { + logger httpexpect.Logger +} + +func (h *httpLog) Logf(fm string, args ...interface{}) { + if !isRunningOnCI { + h.logger.Logf(fm, args...) + } +} + func createInsecureClientWithExpect(t *testing.T, baseURL string) *httpexpect.Expect { config := httpexpect.Config{ BaseURL: baseURL, Client: createInsecureClient(), Reporter: httpexpect.NewAssertReporter(t), Printers: []httpexpect.Printer{ - httpexpect.NewCompactPrinter(t), + httpexpect.NewCurlPrinter(&httpLog{t}), }, } return httpexpect.WithConfig(config) @@ -53,6 +66,13 @@ func createDocument(e *httpexpect.Expect, auth string, documentType string, stat return obj } +func failedUpdateDocument(e *httpexpect.Expect, auth string, documentType string, status int, docIdentifier string, payload map[string]interface{}) *httpexpect.Object { + obj := addCommonHeaders(e.PUT("/"+documentType+"/"+docIdentifier), auth). + WithJSON(payload). + Expect().Status(status).JSON().Object() + return obj +} + func updateDocument(e *httpexpect.Expect, auth string, documentType string, status int, docIdentifier string, payload map[string]interface{}) *httpexpect.Object { obj := addCommonHeaders(e.PUT("/"+documentType+"/"+docIdentifier), auth). WithJSON(payload). @@ -126,23 +146,23 @@ func createInsecureClient() *http.Client { return &http.Client{Transport: tr} } -func waitTillStatus(t *testing.T, e *httpexpect.Expect, auth string, txID string, expectedStatus string) { +func getTransactionStatusAndMessage(e *httpexpect.Expect, auth string, txID string) (string, string) { for { - resp := addCommonHeaders(e.GET("/transactions/"+txID), auth).Expect().Status(200).JSON().Object() - status := resp.Path("$.status").String().Raw() + resp := addCommonHeaders(e.GET("/transactions/"+txID), auth).Expect().Status(200).JSON().Object().Raw() + status := resp["status"].(string) if status == "pending" { time.Sleep(1 * time.Second) continue } - if status == expectedStatus { - break - } else { - t.Error(resp.Path("$.message").String().Raw()) + message, ok := resp["message"].(string) + + if !ok { + message = "Unknown error while processing transaction" } - break + return status, message } } diff --git a/testworld/nft_test.go b/testworld/nft_test.go index 11c02d37c..bd7347ab8 100644 --- a/testworld/nft_test.go +++ b/testworld/nft_test.go @@ -3,9 +3,13 @@ package testworld import ( + "fmt" "net/http" "testing" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/documents" "github.com/stretchr/testify/assert" @@ -69,8 +73,10 @@ func paymentObligationMint(t *testing.T, documentType string, grantNFTAccess, to // Alice shares document with Bob res := createDocument(alice.httpExpect, alice.id.String(), documentType, http.StatusOK, defaultNFTPayload(documentType, []string{bob.id.String()})) txID := getTransactionID(t, res) - - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -88,6 +94,17 @@ func paymentObligationMint(t *testing.T, documentType string, grantNFTAccess, to if proofPrefix == typePO { proofPrefix = poPrefix } + acc, err := alice.host.configService.GetAccount(alice.id[:]) + if err != nil { + t.Error(err) + } + keys, err := acc.GetKeys() + if err != nil { + t.Error(err) + } + signerId := hexutil.Encode(append(alice.id[:], keys[identity.KeyPurposeSigning.Name].PublicKey...)) + signingRoot := fmt.Sprintf("%s.%s", documents.DRTreePrefix, documents.SigningRootField) + signatureSender := fmt.Sprintf("%s.signatures[%s].signature", documents.SignaturesTreePrefix, signerId) // mint an NFT test := struct { @@ -100,7 +117,7 @@ func paymentObligationMint(t *testing.T, documentType string, grantNFTAccess, to "identifier": docIdentifier, "registryAddress": doctorFord.getHost("Alice").config.GetContractAddress(config.PaymentObligation).String(), "depositAddress": "0x44a0579754d6c94e7bb2c26bfa7394311cc50ccb", // Centrifuge address - "proofFields": []string{proofPrefix + ".gross_amount", proofPrefix + ".currency", proofPrefix + ".due_date", proofPrefix + ".sender", proofPrefix + ".invoice_status", documents.CDTreePrefix + ".next_version"}, + "proofFields": []string{proofPrefix + ".gross_amount", proofPrefix + ".currency", proofPrefix + ".due_date", proofPrefix + ".sender", proofPrefix + ".invoice_status", signingRoot, signatureSender, documents.CDTreePrefix + ".next_version"}, "submitTokenProof": tokenProof, "submitNftOwnerAccessProof": nftReadAccessProof, "grantNftAccess": grantNFTAccess, @@ -109,7 +126,10 @@ func paymentObligationMint(t *testing.T, documentType string, grantNFTAccess, to response, err := alice.host.mintNFT(alice.httpExpect, alice.id.String(), test.httpStatus, test.payload) txID = getTransactionID(t, response) - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message = getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } assert.Nil(t, err, "mintNFT should be successful") assert.True(t, len(response.Value("token_id").String().Raw()) > 0, "successful tokenId should have length 77") diff --git a/testworld/park.go b/testworld/park.go index dd9ca45bb..01f3d8897 100644 --- a/testworld/park.go +++ b/testworld/park.go @@ -15,6 +15,7 @@ import ( "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers" "github.com/centrifuge/go-centrifuge/cmd" "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/node" @@ -33,6 +34,7 @@ var hostConfig = []struct { {"Bob", 8085, 38205, true}, {"Charlie", 8086, 38206, true}, {"Kenny", 8087, 38207, false}, + {"Eve", 8088, 38208, false}, } const defaultP2PTimeout = "10s" @@ -65,8 +67,6 @@ type hostManager struct { // tempHosts are hosts created at runtime, they should be part of niceHosts/naughtyHosts as well tempHosts map[string]*host - // TODO create evil hosts such as William (or Eve) - // canc is the cancel signal for all hosts canc context.CancelFunc @@ -237,6 +237,10 @@ type host struct { createConfig bool multiAccount bool accounts []string + p2pClient documents.Client + anchorProcessor documents.AnchorProcessor + docSrv documents.Service + configService config.Service } func newHost( @@ -288,9 +292,10 @@ func (h *host) init() error { } h.identity = identity.NewDIDFromBytes(idBytes) h.idService = h.bootstrappedCtx[identity.BootstrappedDIDService].(identity.ServiceDID) - if err != nil { - return err - } + h.p2pClient = h.bootstrappedCtx[bootstrap.BootstrappedPeer].(documents.Client) + h.anchorProcessor = h.bootstrappedCtx[documents.BootstrappedAnchorProcessor].(documents.AnchorProcessor) + h.docSrv = h.bootstrappedCtx[documents.BootstrappedDocumentService].(documents.Service) + h.configService = h.bootstrappedCtx[config.BootstrappedConfigStorage].(config.Service) return nil } @@ -313,10 +318,10 @@ func (h *host) live(c context.Context) error { signal.Notify(controlC, os.Interrupt) select { case err := <-feedback: - log.Info(h.name+" encountered error ", err) + log.Errorf("%s encountered error %v", h.name, err) return err case sig := <-controlC: - log.Info(h.name+" shutting down because of ", sig) + log.Errorf("%s shutting down because of %s", h.name, sig.String()) canc() err := <-feedback return err diff --git a/testworld/park_test.go b/testworld/park_test.go index b562681aa..b8458dd24 100644 --- a/testworld/park_test.go +++ b/testworld/park_test.go @@ -13,6 +13,8 @@ import ( func TestHost_Happy(t *testing.T) { t.Parallel() + + // Hosts alice := doctorFord.getHostTestSuite(t, "Alice") bob := doctorFord.getHostTestSuite(t, "Bob") charlie := doctorFord.getHostTestSuite(t, "Charlie") @@ -20,7 +22,10 @@ func TestHost_Happy(t *testing.T) { // alice shares a document with bob and charlie res := createDocument(alice.httpExpect, alice.id.String(), typeInvoice, http.StatusOK, defaultInvoicePayload([]string{bob.id.String(), charlie.id.String()})) txID := getTransactionID(t, res) - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) @@ -39,6 +44,7 @@ func TestHost_Happy(t *testing.T) { func TestHost_RestartWithAccounts(t *testing.T) { t.Parallel() + // Name can be randomly generated tempHostName := "Sleepy" bootnode, err := doctorFord.bernard.p2pURL() diff --git a/testworld/proof_test.go b/testworld/proof_test.go index d3ce607a2..cb67d1cb9 100644 --- a/testworld/proof_test.go +++ b/testworld/proof_test.go @@ -12,13 +12,11 @@ import ( func TestProofWithMultipleFields_invoice_successful(t *testing.T) { t.Parallel() proofWithMultipleFields_successful(t, typeInvoice) - } func TestProofWithMultipleFields_po_successful(t *testing.T) { t.Parallel() proofWithMultipleFields_successful(t, typePO) - } func proofWithMultipleFields_successful(t *testing.T, documentType string) { @@ -28,7 +26,10 @@ func proofWithMultipleFields_successful(t *testing.T, documentType string) { // Alice shares a document with Bob res := createDocument(alice.httpExpect, alice.id.String(), documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{bob.id.String()})) txID := getTransactionID(t, res) - waitTillStatus(t, alice.httpExpect, alice.id.String(), txID, "success") + status, message := getTransactionStatusAndMessage(alice.httpExpect, alice.id.String(), txID) + if status != "success" { + t.Error(message) + } docIdentifier := getDocumentIdentifier(t, res) if docIdentifier == "" { @@ -42,7 +43,6 @@ func proofWithMultipleFields_successful(t *testing.T, documentType string) { checkProof(proofFromAlice, documentType, docIdentifier) checkProof(proofFromBob, documentType, docIdentifier) - } func checkProof(objProof *httpexpect.Object, documentType string, docIdentifier string) { @@ -61,5 +61,4 @@ func checkProof(objProof *httpexpect.Object, documentType string, docIdentifier objProof.Path("$.field_proofs[0].sorted_hashes").NotNull() objProof.Path("$.field_proofs[1].property").String().Equal(compactPrefix + prop2) objProof.Path("$.field_proofs[1].sorted_hashes").NotNull() - } diff --git a/testworld/signature_test.go b/testworld/signature_test.go new file mode 100644 index 000000000..07fceda7e --- /dev/null +++ b/testworld/signature_test.go @@ -0,0 +1,180 @@ +// +build testworld + +package testworld + +import ( + "context" + "net/http" + "testing" + + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" + "github.com/centrifuge/go-centrifuge/crypto" + "github.com/centrifuge/go-centrifuge/crypto/secp256k1" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/documents/purchaseorder" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/centrifuge/go-centrifuge/testingutils/documents" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" +) + +func TestHost_ValidSignature(t *testing.T) { + // Hosts + bob := doctorFord.getHostTestSuite(t, "Bob") + eve := doctorFord.getHostTestSuite(t, "Eve") + + ctxh := testingconfig.CreateAccountContext(t, eve.host.config) + + // Get PublicKey and PrivateKey + publicKey, privateKey := GetSigningKeyPair(t, eve.host.idService, eve.id, ctxh) + + collaborators := [][]byte{bob.id[:]} + dm := createCDWithEmbeddedPO(t, collaborators, eve.id, publicKey, privateKey) + + signatures, signatureErrors, err := eve.host.p2pClient.GetSignaturesForDocument(ctxh, dm) + assert.NoError(t, err) + assert.Nil(t, signatureErrors) + assert.Equal(t, 1, len(signatures)) +} + +func TestHost_FakedSignature(t *testing.T) { + // Hosts + alice := doctorFord.getHostTestSuite(t, "Alice") + bob := doctorFord.getHostTestSuite(t, "Bob") + eve := doctorFord.getHostTestSuite(t, "Eve") + + actxh := testingconfig.CreateAccountContext(t, alice.host.config) + ectxh := testingconfig.CreateAccountContext(t, eve.host.config) + + // Get PublicKey and PrivateKey + publicKey, privateKey := GetSigningKeyPair(t, alice.host.idService, alice.id, actxh) + + collaborators := [][]byte{bob.id[:]} + dm := createCDWithEmbeddedPO(t, collaborators, eve.id, publicKey, privateKey) + + signatures, signatureErrors, err := eve.host.p2pClient.GetSignaturesForDocument(ectxh, dm) + assert.NoError(t, err) + assert.Error(t, signatureErrors[0], "Signature verification failed error") + assert.Equal(t, 0, len(signatures)) +} + +func TestHost_RevokedSigningKey(t *testing.T) { + // Hosts + bob := doctorFord.getHostTestSuite(t, "Bob") + eve := doctorFord.getHostTestSuite(t, "Eve") + + ctxh := testingconfig.CreateAccountContext(t, eve.host.config) + + // Get PublicKey and PrivateKey + publicKey, privateKey := GetSigningKeyPair(t, eve.host.idService, eve.id, ctxh) + + // Revoke Key + key, err := utils.SliceToByte32(publicKey) + assert.NoError(t, err) + RevokeKey(t, eve.host.idService, key, eve.id, ctxh) + + // Eve creates document with Bob and signs with Revoked key + collaborators := [][]byte{bob.id[:]} + dm := createCDWithEmbeddedPO(t, collaborators, eve.id, publicKey, privateKey) + + signatures, signatureErrors, err := eve.host.p2pClient.GetSignaturesForDocument(ctxh, dm) + assert.NoError(t, err) + assert.Error(t, signatureErrors[0], "Signature verification failed error") + assert.Equal(t, 0, len(signatures)) + + // Bob creates document with Eve whose key is revoked + keys, err := eve.host.idService.GetKeysByPurpose(eve.id, &(identity.KeyPurposeSigning.Value)) + assert.NoError(t, err) + + // Revoke Key + RevokeKey(t, eve.host.idService, keys[0].GetKey(), eve.id, ctxh) + + res := createDocument(bob.httpExpect, bob.id.String(), typeInvoice, http.StatusOK, defaultInvoicePayload([]string{eve.id.String()})) + txID := getTransactionID(t, res) + status, message := getTransactionStatusAndMessage(bob.httpExpect, bob.id.String(), txID) + if status != "failed" { + t.Error(message) + } + assert.Contains(t, message, "failed to validate signatures") +} + +// Helper Methods +func createCDWithEmbeddedPO(t *testing.T, collaborators [][]byte, identityDID identity.DID, publicKey []byte, privateKey []byte) documents.Model { + payload := testingdocuments.CreatePOPayload() + var cs []string + for _, c := range collaborators { + cs = append(cs, hexutil.Encode(c)) + } + payload.Collaborators = cs + + po := new(purchaseorder.PurchaseOrder) + err := po.InitPurchaseOrderInput(payload, identityDID.String()) + assert.NoError(t, err) + + err = po.AddUpdateLog(identityDID) + assert.NoError(t, err) + + _, err = po.CalculateDataRoot() + assert.NoError(t, err) + + sr, err := po.CalculateSigningRoot() + assert.NoError(t, err) + + s, err := crypto.SignMessage(privateKey, sr, crypto.CurveSecp256K1) + assert.NoError(t, err) + + sig := &coredocumentpb.Signature{ + SignatureId: append(identityDID[:], publicKey...), + SignerId: identityDID[:], + PublicKey: publicKey, + Signature: s, + } + po.AppendSignatures(sig) + + _, err = po.CalculateDocumentRoot() + assert.NoError(t, err) + + return po +} + +func RevokeKey(t *testing.T, idService identity.ServiceDID, key [32]byte, identityDID identity.DID, ctx context.Context) { + idService.RevokeKey(ctx, key) + response, err := idService.GetKey(identityDID, key) + assert.NoError(t, err) + assert.NotEqual(t, utils.ByteSliceToBigInt([]byte{0}), response.RevokedAt, "Revoked key successfully") +} + +func AddKey(t *testing.T, idService identity.ServiceDID, testKey identity.KeyDID, identityDID identity.DID, ctx context.Context) { + err := idService.AddKey(ctx, testKey) + assert.Nil(t, err, "Add Key should be successful") + + _, err = idService.GetKey(identityDID, testKey.GetKey()) + assert.Nil(t, err, "Get Key should be successful") + + err = idService.ValidateKey(ctx, identityDID, utils.Byte32ToSlice(testKey.GetKey()), testKey.GetPurpose(), nil) + assert.Nil(t, err, "Key with purpose should exist") +} + +func GetSigningKeyPair(t *testing.T, idService identity.ServiceDID, identityDID identity.DID, ctx context.Context) ([]byte, []byte) { + // Generate PublicKey and PrivateKey + publicKey, privateKey, err := secp256k1.GenerateSigningKeyPair() + assert.NoError(t, err) + + address32Bytes := convertKeyTo32Bytes(publicKey) + + // Test Key + testKey := identity.NewKey(address32Bytes, &(identity.KeyPurposeSigning.Value), utils.ByteSliceToBigInt([]byte{123}), 0) + + // Add Key + AddKey(t, idService, testKey, identityDID, ctx) + + return utils.Byte32ToSlice(address32Bytes), privateKey +} + +func convertKeyTo32Bytes(key []byte) [32]byte { + address := common.HexToAddress(secp256k1.GetAddress(key)) + return utils.AddressTo32Bytes(address) +} diff --git a/testworld/start_test.go b/testworld/start_test.go index 08a4027dd..7695a3b4d 100644 --- a/testworld/start_test.go +++ b/testworld/start_test.go @@ -20,8 +20,6 @@ const ( multiHostMultiAccount testType = "multiHostMultiAccount" ) -var isRunningOnCI = len(os.Getenv("TRAVIS")) != 0 - // doctorFord manages the hosts var doctorFord *hostManager diff --git a/transactions/transaction.go b/transactions/transaction.go index c84943c70..89d4ae85b 100644 --- a/transactions/transaction.go +++ b/transactions/transaction.go @@ -110,6 +110,9 @@ type Transaction struct { // Logs are transaction log messages Logs []Log CreatedAt time.Time + + // Values retrieved from events + Values map[string]TXValue } // JSON returns json marshaled transaction. @@ -136,9 +139,17 @@ func NewTransaction(identity identity.DID, description string) *Transaction { Status: Pending, TaskStatus: make(map[string]Status), CreatedAt: time.Now().UTC(), + Values: make(map[string]TXValue), } } +// TXValue holds the key and value filtered by the transaction +type TXValue struct { + Key string + KeyIdx int + Value []byte +} + // Config is the config interface for transactions package type Config interface { GetEthereumContextWaitTimeout() time.Duration @@ -149,6 +160,7 @@ type Manager interface { // ExecuteWithinTX executes the given unit of work within a transaction ExecuteWithinTX(ctx context.Context, accountID identity.DID, existingTxID TxID, desc string, work func(accountID identity.DID, txID TxID, txMan Manager, err chan<- error)) (txID TxID, done chan bool, err error) GetTransaction(accountID identity.DID, id TxID) (*Transaction, error) + UpdateTransactionWithValue(accountID identity.DID, id TxID, key string, value []byte) error UpdateTaskStatus(accountID identity.DID, id TxID, status Status, taskName, message string) error GetTransactionStatus(accountID identity.DID, id TxID) (*transactionspb.TransactionStatusResponse, error) WaitForTransaction(accountID identity.DID, txID TxID) error diff --git a/transactions/txv1/base_task.go b/transactions/txv1/base_task.go index 3decd99fe..79b41e28c 100644 --- a/transactions/txv1/base_task.go +++ b/transactions/txv1/base_task.go @@ -38,6 +38,11 @@ func (b *BaseTask) ParseTransactionID(taskTypeName string, kwargs map[string]int // UpdateTransaction add a new log and updates the status of the transaction based on the error. func (b *BaseTask) UpdateTransaction(accountID identity.DID, taskTypeName string, err error) error { + return b.UpdateTransactionWithValue(accountID, taskTypeName, err, nil) +} + +// UpdateTransactionWithValue add a new log and updates the status of the transaction based on the error and adds a value to the tx +func (b *BaseTask) UpdateTransactionWithValue(accountID identity.DID, taskTypeName string, err error, txValue *transactions.TXValue) error { if err == gocelery.ErrTaskRetryable { return err } @@ -49,5 +54,11 @@ func (b *BaseTask) UpdateTransaction(accountID identity.DID, taskTypeName string } log.Infof("Task %s successful for transaction:%v\n", taskTypeName, b.TxID.String()) + if txValue != nil { + err = b.TxManager.UpdateTransactionWithValue(accountID, b.TxID, txValue.Key, txValue.Value) + if err != nil { + return err + } + } return b.TxManager.UpdateTaskStatus(accountID, b.TxID, transactions.Success, taskTypeName, "") } diff --git a/transactions/txv1/manager.go b/transactions/txv1/manager.go index 9c1c6ce35..22deaf382 100644 --- a/transactions/txv1/manager.go +++ b/transactions/txv1/manager.go @@ -42,6 +42,15 @@ func (s *manager) GetDefaultTaskTimeout() time.Duration { return s.config.GetEthereumContextWaitTimeout() } +func (s *manager) UpdateTransactionWithValue(accountID identity.DID, id transactions.TxID, key string, value []byte) error { + tx, err := s.GetTransaction(accountID, id) + if err != nil { + return err + } + tx.Values[key] = transactions.TXValue{Key: key, Value: value} + return s.saveTransaction(tx) +} + func (s *manager) UpdateTaskStatus(accountID identity.DID, id transactions.TxID, status transactions.Status, taskName, message string) error { tx, err := s.GetTransaction(accountID, id) if err != nil { @@ -165,10 +174,15 @@ func (s *manager) GetTransactionStatus(accountID identity.DID, id transactions.T lastUpdated = log.CreatedAt.UTC() } + tm, err := utils.ToTimestamp(lastUpdated) + if err != nil { + return nil, err + } + return &transactionspb.TransactionStatusResponse{ TransactionId: tx.ID.String(), Status: string(tx.Status), Message: msg, - LastUpdated: utils.ToTimestamp(lastUpdated), + LastUpdated: tm, }, nil } diff --git a/transactions/txv1/manager_test.go b/transactions/txv1/manager_test.go index 0002dded4..5d2a8c042 100644 --- a/transactions/txv1/manager_test.go +++ b/transactions/txv1/manager_test.go @@ -92,7 +92,9 @@ func TestService_GetTransaction(t *testing.T) { assert.Equal(t, txs.TransactionId, txn.ID.String()) assert.Equal(t, string(transactions.Pending), txs.Status) assert.Empty(t, txs.Message) - assert.Equal(t, utils.ToTimestamp(txn.CreatedAt), txs.LastUpdated) + tm, err := utils.ToTimestamp(txn.CreatedAt) + assert.NoError(t, err) + assert.Equal(t, tm, txs.LastUpdated) log := transactions.NewLog("action", "some message") txn.Logs = append(txn.Logs, log) @@ -106,7 +108,9 @@ func TestService_GetTransaction(t *testing.T) { assert.Equal(t, txs.TransactionId, txn.ID.String()) assert.Equal(t, string(transactions.Success), txs.Status) assert.Equal(t, log.Message, txs.Message) - assert.Equal(t, utils.ToTimestamp(log.CreatedAt), txs.LastUpdated) + tm, err = utils.ToTimestamp(log.CreatedAt) + assert.NoError(t, err) + assert.Equal(t, tm, txs.LastUpdated) } func TestService_CreateTransaction(t *testing.T) { diff --git a/utils/time.go b/utils/time.go index 1338b04c9..62bf5daa6 100644 --- a/utils/time.go +++ b/utils/time.go @@ -3,13 +3,16 @@ package utils import ( "time" + "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/timestamp" ) // ToTimestamp converts time.Time to timestamp.TimeStamp. -func ToTimestamp(time time.Time) *timestamp.Timestamp { - return ×tamp.Timestamp{ - Seconds: int64(time.Second()), - Nanos: int32(time.Nanosecond()), - } +func ToTimestamp(time time.Time) (*timestamp.Timestamp, error) { + return ptypes.TimestampProto(time) +} + +// FromTimestamp converts a timestamp protobuf to time +func FromTimestamp(t *timestamp.Timestamp) (time.Time, error) { + return ptypes.Timestamp(t) } diff --git a/utils/time_test.go b/utils/time_test.go index 8bb0fe0b7..80fcbb22e 100644 --- a/utils/time_test.go +++ b/utils/time_test.go @@ -11,8 +11,9 @@ import ( func TestToTimestamp(t *testing.T) { now := time.Now().UTC() - ts := ToTimestamp(now) + ts, err := ToTimestamp(now) + assert.NoError(t, err) assert.NotNil(t, ts, "must be non nil") - assert.Equal(t, now.Second(), int(ts.Seconds)) + assert.Equal(t, now.Unix(), ts.Seconds) assert.Equal(t, now.Nanosecond(), int(ts.Nanos)) }