diff --git a/.gitignore b/.gitignore index 3ab321839..e16b3c59a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage/ coverage.txt *.swp *.swo +testworld/peerconfigs/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 53d1fdccd..c44c398d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.10.x + - 1.11.4 # Maybe this helps with building branches in a fork go_import_path: github.com/centrifuge/go-centrifuge @@ -11,9 +11,6 @@ services: env: matrix: - DEPCACHEDIR=/tmp/depcache IMAGE_NAME=centrifugeio/go-centrifuge PROTOTOOL_VERSION=0.4.0 PROTOTOOL_BIN=~/bin/0.4.0/prototool - global: - - secure: XDK6tU10HRQOLqYBquB69KJ0ZcA/avqWZ3o73ET+RurApvlYqEY1YA1FtPs2+vY9BlvvzN0c7v9RoxIqwYsYl7m/qoZHBcl4gpjCcFJtjPqpudkrigFZmb5voSelqKXdh4UmwQa4hQPq4W002eju82FHXEtdFSThNi1jAPMitLEmFhG22JoKda2M810HbPqrBctye6RD/2ZSsSLMZQgeTDkwvU5FFDzO2qfKQqtYrMqfpvcqTsfqr6vpUyZyaOVsr2AzLtu3qLB9UBSHw98yOoSsTpZ+rg6d2X5PWn8ndOKSyswelHoPCdNjTwPOxRie+P7tEClr90LtdWrtXbQDxp0j6dutP0FQALv38sSioazag0sDePl6yjGvSH9s8UgvnpSIHUZsS+ZxntVEOSNnxzP+wFwP05XEpzd5XL+dn9CjH8PtXOX+lv+KHc8NGHeKdnw/eiwD5eikW13V9u6Fm2ZyAOeLSqXb+m0Hfj0+OirUnisZII1dO2AAWbigop7tk0xn/UPQQaT/TEREdhM9cSXhQOyHB4UoHac7zipbDKtBveJPbaGNe9iRqPaLViPO9XwUAHkOGrNv8gl+WaNzrmiOFgWpVg7KUUxzKu5n2QyuNZb5H27+u4RpUbv6qTQiU0VhSW9F2xcwgHLVoJGSDVwyOM1XuTidX/aTQ/G0pZQ= - - secure: Id6nD7LQ//FySnKgFAae4Gi60JEnpXw1lWARfcJbYOVIH7usbyKm+lHyyCb9tsj5eJmdHovpzd4ZO52arMdDbvzGmnBHbWMXhjJe7oGJNgJ4ifI0GTSCCHOshox5kzhom3OekjOs5QZgcg/DN0Aflt5JkJY+PQy0KGqSo449uvB86QMesrDI2FVZlCL/a81HaSRIB9UhWfgyQrpEY6CqmIQtWY4MTHPnb1YegG04OKrUVdmD0iPbN3diDciiS5xCXi4JyiP+hSbUbnAakEr071CptRaGiJXkCkMK28AUvn5ox6xffDNeA2snDfE36EUUZYLkcs89fUKAp8Quu4i18HTFCtfC8o82mt2nAu+ZNEuU+SJzK4mKUwdSwt/kDMMxdHZ5ss7aoTlreHGDehts12OGBMsp7w/cYqKnnaLf8uHFfcasOD+51SDGyo5jzA5QDM6lrt28Cptb/3NwH+xmFTY85YxE9tnTAKkBd/NRPlB8Q1HNzLWu4QGC4u0wnWeM0IvWhtEFguEb8E9MtcvKHHC5iVyyxJUavJ9s+41Xg4x0KF2Z0fmmsZTu1WYrcFLhWtBnkUGeS70NfQpGZ5UkuL9yJN3SvVApdku9QMbw6Bn/ep05V5JnFSOGlHx5HztyMpkq/CJRcuzq1lxnoUWhXVulhwBrDelvsgO8pBVALJw= checkout: post: @@ -38,7 +35,7 @@ jobs: include: - stage: test script: - - make proto-lint proto-all gen-swagger generate format-go + - 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 after_success: diff --git a/Dockerfile b/Dockerfile index d8699b68f..a641718df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ -FROM golang:1.10-alpine as builder +FROM golang:1.11-alpine as builder RUN apk update && apk add --no-cache openssh git jq curl gcc libc-dev build-base ADD . /go/src/github.com/centrifuge/go-centrifuge WORKDIR /go/src/github.com/centrifuge/go-centrifuge -RUN go install -ldflags "-X github.com/centrifuge/go-centrifuge/version.gitCommit=`git rev-parse HEAD`" ./... +RUN go install -ldflags "-X github.com/centrifuge/go-centrifuge/version.gitCommit=`git rev-parse HEAD`" ./cmd/centrifuge/... FROM alpine:latest RUN apk update && apk add --no-cache jq curl WORKDIR /root/ -COPY --from=builder /go/bin/go-centrifuge . +COPY --from=builder /go/bin/centrifuge . COPY build/scripts/docker/entrypoint.sh /root VOLUME ["/root/config"] diff --git a/Gopkg.lock b/Gopkg.lock index c5a7f5dc5..4686ade13 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -21,6 +21,25 @@ pruneopts = "UT" revision = "5312a61534124124185d41f09206b9fef1d88403" +[[projects]] + digest = "1:2aff5edb9bccd2974090fddb17ca7ab05a3f5c983db567c30c7f0b53404f5783" + name = "github.com/ajg/form" + packages = ["."] + pruneopts = "UT" + revision = "cc2954064ec9ea8d93917f0f87456e11d7b881ad" + version = "v1.5" + +[[projects]] + digest = "1:1156cfea0ff969858f6027df95c15ca5e802556b466aebeedaf53dde3b301db3" + name = "github.com/allegro/bigcache" + packages = [ + ".", + "queue", + ] + pruneopts = "UT" + revision = "f31987a23e44c5121ef8c8b2f2ea2e8ffa37b068" + version = "v1.1.0" + [[projects]] branch = "master" digest = "1:7d191fd0c54ff370eaf6116a14dafe2a328df487baea280699f597aae858d00d" @@ -38,16 +57,14 @@ revision = "cff30e1d23fc9e800b2b5b4b41ef1817dda07e9f" [[projects]] - branch = "develop" - digest = "1:2ad07f3fadd7fbed5895432782ffd4c427b766a1237b979590edbcc02d78ba68" + digest = "1:4542fc4bb0f0bab4d9fad7bf85ca7152cf706eb0ccb1f501899f625af6865d34" name = "github.com/centrifuge/centrifuge-ethereum-contracts" packages = ["."] pruneopts = "T" - revision = "c60964592c1a2b83c5676878c937cdbe6719869d" - source = "git@github.com:centrifuge/centrifuge-ethereum-contracts" + revision = "978840734a4a2fccd5d8dc7e462d9108079c5b5d" [[projects]] - digest = "1:c6e2ed36c71d00b36eb36fa5b7aad8c06a1c07f58701557c4bb7b8e0084356d8" + digest = "1:d12e66a9500785cff8a8245bd8a138efff96ed3c1d0551b845cecdc25af57f90" name = "github.com/centrifuge/centrifuge-protobufs" packages = [ "documenttypes", @@ -59,8 +76,7 @@ "gen/go/purchaseorder", ] pruneopts = "T" - revision = "838c11114163b6152d7b56574536f046b858e68d" - source = "git@github.com:centrifuge/centrifuge-protobufs" + revision = "34ce7eaff0b89ac4a34e60654d34297747e5a7b4" [[projects]] digest = "1:c925cd40d25fe72f5916e010b79b5d9f6e0e97ec9b190fa658c6c67b913229ea" @@ -68,18 +84,16 @@ packages = ["."] pruneopts = "UT" revision = "80f35a18b9a0c632d7843e7e536873b0536f0040" - source = "git@github.com:centrifuge/gocelery" [[projects]] - digest = "1:9ecc8c63be5b2a2e9c4daefc265ea476757d91a04394fbb984bcdb3c652129da" + digest = "1:f84321c62605dab82f35a0795dfe30981199e28ea8a0652e5e7ba9af66575fef" name = "github.com/centrifuge/precise-proofs" packages = [ "proofs", "proofs/proto", ] pruneopts = "UT" - revision = "c673e56ee0de0d721a10dee5cd2aa59015e4420a" - source = "git@github.com:centrifuge/precise-proofs" + revision = "d0df6e2ed059de47375ef8281529a500cd920261" [[projects]] digest = "1:0ef770954bca104ee99b3b6b7f9b240605ac03517d9f98cbc1893daa03f3c038" @@ -106,7 +120,7 @@ version = "v1.7" [[projects]] - digest = "1:ba11b65320bfa1a5e8e43b050833bca23490e508a67c45d7e430156cefc2ab7f" + digest = "1:54f69b2b6585c979160e3b00194fcdf343abf0f439a9ae5a7d40dd223c7f7364" name = "github.com/ethereum/go-ethereum" packages = [ ".", @@ -118,6 +132,7 @@ "common/hexutil", "common/math", "common/mclock", + "common/prque", "core/types", "crypto", "crypto/secp256k1", @@ -134,9 +149,15 @@ "trie", ] pruneopts = "T" - revision = "89451f7c382ad2185987ee369f16416f89c28a7d" - source = "git@github.com:ethereum/go-ethereum" - version = "v1.8.15" + revision = "d2328b604a2d4ecdccc47d8f9133593161cbd40a" + +[[projects]] + digest = "1:af43bdaaf86655a2343f113e9b293bbc16b12099eaeb223982bbe4d4c22ba14d" + name = "github.com/fatih/structs" + packages = ["."] + pruneopts = "UT" + revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047" + version = "v1.1.0" [[projects]] digest = "1:d1341a04a4443bba8c8af76ec6c3960206074490a1284b4853ff5a88a66c63b7" @@ -154,6 +175,22 @@ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" +[[projects]] + branch = "master" + digest = "1:99409b277e9cd3f00284f8898e43b517956514108d1b220efc60ef05c8a8fd58" + name = "github.com/gavv/httpexpect" + packages = ["."] + pruneopts = "UT" + revision = "bdde308713130a703436e014ff782958c251d20a" + +[[projects]] + branch = "master" + digest = "1:fbc02c2b3b78e6bc30228f3bae582f1fcea74a050cbdbf42f24eff12bef8eab4" + name = "github.com/gavv/monotime" + packages = ["."] + pruneopts = "UT" + revision = "6f8212e8d10df7383609d3c377ca08884d8f3ec0" + [[projects]] digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda" name = "github.com/ghodss/yaml" @@ -238,6 +275,14 @@ revision = "9c11da706d9b7902c6da69c592f75637793fe121" version = "v2.0.0" +[[projects]] + digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690" + name = "github.com/google/go-querystring" + packages = ["query"] + pruneopts = "UT" + revision = "44c6ddd0a2342c386950e880b658017258da92fc" + version = "v1.0.0" + [[projects]] digest = "1:3a26588bc48b96825977c1b3df964f8fd842cd6860cc26370588d3563433cf11" name = "github.com/google/uuid" @@ -271,7 +316,6 @@ ] pruneopts = "T" revision = "8558711daa6c2853489043207b563dceacbc19cf" - source = "git@github.com:grpc-ecosystem/grpc-gateway" version = "v1.5.0" [[projects]] @@ -298,22 +342,6 @@ pruneopts = "UT" revision = "d9f6b97f8db22dd1e090fd0bbbe98f09cc7dd0a8" -[[projects]] - digest = "1:0ade334594e69404d80d9d323445d2297ff8161637f9b2d347cc6973d2d6f05b" - name = "github.com/hashicorp/errwrap" - packages = ["."] - pruneopts = "UT" - revision = "8a6fb523712970c966eefc6b39ed2c5e74880354" - version = "v1.0.0" - -[[projects]] - digest = "1:f668349b83f7d779567c880550534addeca7ebadfdcf44b0b9c39be61864b4b7" - name = "github.com/hashicorp/go-multierror" - packages = ["."] - pruneopts = "T" - revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1" - version = "v1.0.0" - [[projects]] digest = "1:8ec8d88c248041a6df5f6574b87bc00e7e0b493881dad2e7ef47b11dc69093b5" name = "github.com/hashicorp/golang-lru" @@ -361,12 +389,12 @@ revision = "1395d1447324cbea88d249fbfcfd70ea878fdfca" [[projects]] - branch = "master" - digest = "1:89cd2208bdeb192cc83ee287216af3c987fc87a90b9087718ad6631b636cefcf" - name = "github.com/iancoleman/strcase" + digest = "1:a3ce4de79566c21e93cb6934797fdaa587ad3fc6a964708ab77babe54ea67188" + name = "github.com/imkira/go-interpol" packages = ["."] pruneopts = "UT" - revision = "3605ed457bf7f8caa1371b4fafadadc026673479" + revision = "5accad8134979a6ac504d456a6c7f1c53da237ca" + version = "v1.1.0" [[projects]] digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" @@ -477,6 +505,26 @@ pruneopts = "UT" revision = "b497e2f366b8624394fb2e89c10ab607bebdde0b" +[[projects]] + digest = "1:aaa8e0e7e35d92e21daed3f241832cee73d15ca1cd3302ba3843159a959a7eac" + name = "github.com/klauspost/compress" + packages = [ + "flate", + "gzip", + "zlib", + ] + pruneopts = "UT" + revision = "30be6041bed523c18e269a700ebd9c2ea9328574" + version = "v1.4.1" + +[[projects]] + digest = "1:2d643962fac133904694fffa959bc3c5dcfdcee38c6f5ffdd99a3c93eb9c835c" + name = "github.com/klauspost/cpuid" + packages = ["."] + pruneopts = "UT" + revision = "e7e905edc00ea8827e58662220139109efea09db" + version = "v1.2.0" + [[projects]] digest = "1:f3f143e9085a1fbff293e8f131f35c6f9d6b2023002fd719a08655b1a9c6e745" name = "github.com/libp2p/go-addr-util" @@ -847,6 +895,14 @@ revision = "fa473d140ef3c6adf42d6b391fe76707f1f243c8" version = "v1.0.0" +[[projects]] + digest = "1:7aefb397a53fc437c90f0fdb3e1419c751c5a3a165ced52325d5d797edf1aca6" + name = "github.com/moul/http2curl" + packages = ["."] + pruneopts = "UT" + revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" + version = "v1.0.0" + [[projects]] branch = "master" digest = "1:b9b9f43a8a410d633e6547f89e830926741070941f2243d4d3a0bb154f565c9e" @@ -921,7 +977,7 @@ packages = ["."] pruneopts = "UT" revision = "04711f07389ecbca09d7999474dda4a88c955c73" - source = "git@github.com:vedhavyas/go-libp2p-grpc.git" + source = "github.com/vedhavyas/go-libp2p-grpc" [[projects]] digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9" @@ -986,6 +1042,25 @@ pruneopts = "UT" revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" +[[projects]] + branch = "master" + digest = "1:45ed9e09a140c93e437e1e9634258040148f2cd18b28758a21b084002eca8c8c" + name = "github.com/savaki/jq" + packages = [ + ".", + "scanner", + ] + pruneopts = "UT" + revision = "0e6baecebbf8a24a6590d3fc8232165008d6e5d8" + +[[projects]] + digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04" + name = "github.com/sergi/go-diff" + packages = ["diffmatchpatch"] + pruneopts = "UT" + revision = "1744e2970ca51c86172c8190fadad617561ed6e7" + version = "v1.0.0" + [[projects]] digest = "1:919bb3aa6d9d0b67648c219fa4925312bc3c2872da19e818fa769e9c97a2b643" name = "github.com/spaolacci/murmur3" @@ -1062,11 +1137,12 @@ version = "v0.1.1" [[projects]] - digest = "1:15a4a7e5afac3cea801fa24831fce3bf3b5bd3620cbf8355a07b7dbf06877883" + digest = "1:cf4fdb98e23a565bd82473027d37512a3d5b186fba3a1895d7e8401d8ce3ffe1" name = "github.com/stretchr/testify" packages = [ "assert", "mock", + "require", ] pruneopts = "UT" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" @@ -1092,6 +1168,26 @@ pruneopts = "UT" revision = "ae2bd5eed72d46b28834ec3f60db3a3ebedd8dbd" +[[projects]] + digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59" + name = "github.com/valyala/bytebufferpool" + packages = ["."] + pruneopts = "UT" + revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" + version = "v1.0.0" + +[[projects]] + digest = "1:15ad8a80098fcc7a194b9db6b26d74072a852e4faa957848c8118193d3c69230" + name = "github.com/valyala/fasthttp" + packages = [ + ".", + "fasthttputil", + "stackless", + ] + pruneopts = "UT" + revision = "e5f51c11919d4f66400334047b897ef0a94c6f3c" + version = "v20180529" + [[projects]] branch = "master" digest = "1:98fa13beefbf581ec173561adad6374c460631593b4bdcf03adc29cd18e5d2f5" @@ -1178,6 +1274,30 @@ pruneopts = "UT" revision = "cb29a700b01dc3c2fdd743c00cf54685056bb62a" +[[projects]] + branch = "master" + digest = "1:f4e5276a3b356f4692107047fd2890f2fe534f4feeb6b1fd2f6dfbd87f1ccf54" + name = "github.com/xeipuuv/gojsonpointer" + packages = ["."] + pruneopts = "UT" + revision = "4e3ac2762d5f479393488629ee9370b50873b3a6" + +[[projects]] + branch = "master" + digest = "1:dc6a6c28ca45d38cfce9f7cb61681ee38c5b99ec1425339bfc1e1a7ba769c807" + name = "github.com/xeipuuv/gojsonreference" + packages = ["."] + pruneopts = "UT" + revision = "bd5ef7bd5415a7ac448318e64f11a24cd21e594b" + +[[projects]] + digest = "1:41bd4de0a27c0b7affef4083bc8f86501b4c7d891f95809a0fb758a23eb5e78f" + name = "github.com/xeipuuv/gojsonschema" + packages = ["."] + pruneopts = "UT" + revision = "da425ebb7609ba06a0f395fc8a254d1c303364a0" + version = "v1.0" + [[projects]] digest = "1:778500e3634377cb6660543afa4646c12a3a33aefadd3e1d269e24e57d75a3d7" name = "github.com/xsleonard/go-merkle" @@ -1185,6 +1305,33 @@ pruneopts = "UT" revision = "fbb7cafc5ae411e13e718172f5af7917ad6c3ed7" +[[projects]] + branch = "master" + digest = "1:ac3d942a027d57fbfc5c13791cfaaa4b30729674fea88f2e03190b777c2b674e" + name = "github.com/yalp/jsonpath" + packages = ["."] + pruneopts = "UT" + revision = "5cc68e5049a040829faef3a44c00ec4332f6dec7" + +[[projects]] + digest = "1:52ccbcf36804b0beb5677a8994bd4ac740b71d1d6fe38c02b113dabdda51bf6d" + name = "github.com/yudai/gojsondiff" + packages = [ + ".", + "formatter", + ] + pruneopts = "UT" + revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" + version = "1.0.0" + +[[projects]] + branch = "master" + digest = "1:0d4822d3440c9b5992704bb357061fff7ab60daa85d92dec02b81b78e4908db7" + name = "github.com/yudai/golcs" + packages = ["."] + pruneopts = "UT" + revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68" + [[projects]] digest = "1:b5a8fada1cb985bd2b823735e9ec922c4ecd7252880cbea5921d0a9343be648e" name = "golang.org/x/crypto" @@ -1325,14 +1472,6 @@ revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1" version = "v1.15.0" -[[projects]] - branch = "v2" - digest = "1:0a2f4b974a413866afa1b41130d756643841fb9ad661b81c9a6dd9f1364ed19f" - name = "gopkg.in/karalabe/cookiejar.v2" - packages = ["collections/prque"] - pruneopts = "UT" - revision = "8dcd6a7f4951f6ff3ee9cbb919a06d8925822e57" - [[projects]] branch = "v2" digest = "1:3d3f9391ab615be8655ae0d686a1564f3fec413979bb1aaf018bac1ec1bb1cc7" @@ -1383,7 +1522,9 @@ "github.com/ethereum/go-ethereum/crypto/secp256k1", "github.com/ethereum/go-ethereum/ethclient", "github.com/ethereum/go-ethereum/event", + "github.com/ethereum/go-ethereum/log", "github.com/ethereum/go-ethereum/rpc", + "github.com/gavv/httpexpect", "github.com/go-errors/errors", "github.com/golang/protobuf/jsonpb", "github.com/golang/protobuf/proto", @@ -1396,7 +1537,6 @@ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options", "github.com/grpc-ecosystem/grpc-gateway/runtime", "github.com/grpc-ecosystem/grpc-gateway/utilities", - "github.com/hashicorp/go-multierror", "github.com/ipfs/go-cid", "github.com/ipfs/go-datastore", "github.com/ipfs/go-ipfs-addr", @@ -1413,11 +1553,14 @@ "github.com/multiformats/go-multihash", "github.com/paralin/go-libp2p-grpc", "github.com/roboll/go-vendorinstall", + "github.com/savaki/jq", + "github.com/spf13/cast", "github.com/spf13/cobra", "github.com/spf13/viper", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/mock", "github.com/syndtr/goleveldb/leveldb", + "github.com/syndtr/goleveldb/leveldb/util", "github.com/whyrusleeping/go-logging", "golang.org/x/crypto/ed25519", "golang.org/x/net/context", @@ -1425,7 +1568,6 @@ "google.golang.org/genproto/googleapis/api/annotations", "google.golang.org/grpc", "google.golang.org/grpc/codes", - "google.golang.org/grpc/connectivity", "google.golang.org/grpc/credentials", "google.golang.org/grpc/grpclog", "google.golang.org/grpc/status", diff --git a/Gopkg.toml b/Gopkg.toml index db755fc10..51f388f56 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,36 +28,31 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [[constraint]] name = "github.com/centrifuge/centrifuge-protobufs" - source = "git@github.com:centrifuge/centrifuge-protobufs" - revision = "838c11114163b6152d7b56574536f046b858e68d" + revision = "34ce7eaff0b89ac4a34e60654d34297747e5a7b4" [[override]] name = "github.com/centrifuge/centrifuge-ethereum-contracts" - source = "git@github.com:centrifuge/centrifuge-ethereum-contracts" - branch = "develop" + revision = "978840734a4a2fccd5d8dc7e462d9108079c5b5d" [[constraint]] name = "github.com/Masterminds/semver" version = "1.4.2" -[[constraint]] +[[override]] name = "github.com/centrifuge/precise-proofs" - source = "git@github.com:centrifuge/precise-proofs" - revision = "c673e56ee0de0d721a10dee5cd2aa59015e4420a" + revision = "d0df6e2ed059de47375ef8281529a500cd920261" [[constraint]] name = "github.com/centrifuge/gocelery" - source = "git@github.com:centrifuge/gocelery" revision = "80f35a18b9a0c632d7843e7e536873b0536f0040" [[override]] name = "github.com/xsleonard/go-merkle" revision = "fbb7cafc5ae411e13e718172f5af7917ad6c3ed7" -[[constraint]] +[[override]] name = "github.com/ethereum/go-ethereum" - source = "git@github.com:ethereum/go-ethereum" - version = "1.8.15" + revision = "d2328b604a2d4ecdccc47d8f9133593161cbd40a" [[override]] name = "github.com/satori/go.uuid" @@ -77,7 +72,6 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [[constraint]] name = "github.com/grpc-ecosystem/grpc-gateway" - source = "git@github.com:grpc-ecosystem/grpc-gateway" version= "1.5.0" [[constraint]] @@ -222,7 +216,7 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [[constraint]] name = "github.com/paralin/go-libp2p-grpc" - source = "git@github.com:vedhavyas/go-libp2p-grpc.git" + source = "github.com/vedhavyas/go-libp2p-grpc" revision = "04711f07389ecbca09d7999474dda4a88c955c73" [[constraint]] @@ -300,3 +294,10 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r [prune] go-tests = true unused-packages = true +[[constraint]] + branch = "master" + name = "github.com/savaki/jq" + +[[constraint]] + branch = "master" + name = "github.com/gavv/httpexpect" diff --git a/Makefile b/Makefile index 548f0dc76..b9b02c23f 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,12 @@ install-deps: ## Install Dependencies @command -v dep >/dev/null 2>&1 || go get -u github.com/golang/dep/... @dep ensure @npm --prefix ./build install + @curl -L https://git.io/vp6lP | sh + @mv ./bin/* $(GOPATH)/bin/; rm -rf ./bin + +lint-check: ## runs linters on go code + @gometalinter --disable-all --enable=golint --enable=goimports --enable=vet --enable=nakedret \ + --enable=staticcheck --vendor --skip=resources --skip=testingutils --skip=protobufs --deadline=1m ./...; format-go: ## formats go code @goimports -w . @@ -47,7 +53,6 @@ generate: ## autogenerate go files for config vendorinstall: ## Installs all protobuf dependencies with go-vendorinstall go install github.com/centrifuge/go-centrifuge/vendor/github.com/roboll/go-vendorinstall - go-vendorinstall golang.org/x/tools/cmd/goimports go-vendorinstall github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway go-vendorinstall github.com/golang/protobuf/protoc-gen-go go-vendorinstall github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger @@ -55,7 +60,7 @@ vendorinstall: ## Installs all protobuf dependencies with go-vendorinstall install: ## Builds and Install binary for development install: install-deps vendorinstall - @go install ./... + @go install ./cmd/centrifuge/... install-xgo: ## Install XGO @echo "Ensuring XGO is installed" @@ -65,8 +70,8 @@ build-linux-amd64: ## Build linux/amd64 build-linux-amd64: install-xgo @echo "Building amd64 with flags [${LD_FLAGS}]" @mkdir -p build/linux-amd64 - @xgo -dest build/linux-amd64 -targets=linux/amd64 -ldflags=${LD_FLAGS} . - @mv build/linux-amd64/go-centrifuge-linux-amd64 build/linux-amd64/go-centrifuge + @xgo -go 1.11.x -dest build/linux-amd64 -targets=linux/amd64 -ldflags=${LD_FLAGS} ./cmd/centrifuge/ + @mv build/linux-amd64/centrifuge-linux-amd64 build/linux-amd64/centrifuge @tar -zcvf cent-api-linux-amd64-${TAG}.tar.gz -C build/linux-amd64/ . build-docker: ## Build Docker Image diff --git a/README.md b/README.md index ed30c0a11..ead0b0129 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,8 @@ Let it catch up for a while until is fully synced with the remote peer - In go-centrifuge project run: - ./build/scripts/docker/run.sh rinkeby - Wait until node is in sync with remote peer (1-2 hours): - - geth attach ws://localhost:9546 --exec "net.peerCount" > 0 (rinkeby takes additional time to sync as it needs a peer to pull from, and has shortage of full node peers) - - geth attach ws://localhost:9546 --exec "eth.syncing" -> false + - geth attach http://localhost:9545 --exec "net.peerCount" > 0 (rinkeby takes additional time to sync as it needs a peer to pull from, and has shortage of full node peers) + - geth attach http://localhost:9545 --exec "eth.syncing" -> false - Run tests: - To run only integration tests: - CENT_CENTRIFUGENETWORK='russianhill' TEST_TARGET_ENVIRONMENT='rinkeby' CENT_ETHEREUM_ACCOUNTS_MAIN_KEY='$JSON_KEY' CENT_ETHEREUM_ACCOUNTS_MAIN_PASSWORD="$PASS" CENT_ETHEREUM_ACCOUNTS_MAIN_ADDRESS="$ADDR" ./build/scripts/tests/run_integration_tests.sh @@ -229,3 +229,10 @@ Generating go bindings and swagger with the following command ```bash make proto-all ``` + +## Kovan FAQ + +- With infura you get an error - "This request is not supported because your node is running with state pruning. Run with --pruning=archive.", + what to do? Run a local parity node with kovan eg: with `parity --chain=kovan --port=30304 --warp --warp-barrier 5680000 --no-ancient-blocks --no-serve-light --max-peers 250 --snapshot-peers 50 --min-peers 50 --mode active --tracing off --pruning=archive --db-compaction ssd --cache-size 4096 --jsonrpc-hosts all --jsonrpc-interface all` +- With local parity node you get an error - "Blocked connection to WebSockets server from untrusted origin: .." + what to do? Run the parity node with `--unsafe-expose` flag diff --git a/anchors/anchor.go b/anchors/anchor.go index ece433530..1c1d739e5 100644 --- a/anchors/anchor.go +++ b/anchors/anchor.go @@ -1,103 +1,132 @@ package anchors import ( - "errors" "math/big" + "github.com/centrifuge/go-centrifuge/errors" + + "time" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) const ( - AnchorIDLength = 32 - RootLength = 32 + // AnchorIDLength is the length in bytes of the AnchorID + AnchorIDLength = 32 + + // DocumentRootLength is the length in bytes of the DocumentRoot + DocumentRootLength = 32 + + // DocumentProofLength is the length in bytes of a single proof DocumentProofLength = 32 + + // AnchorSchemaVersion as stored on public repository + AnchorSchemaVersion uint = 1 ) +// AnchorID type is byte array of length AnchorIDLength type AnchorID [AnchorIDLength]byte -func NewAnchorID(anchorBytes []byte) (AnchorID, error) { - var anchorBytesFixed [AnchorIDLength]byte - if !utils.IsValidByteSliceForLength(anchorBytes, AnchorIDLength) { - return anchorBytesFixed, errors.New("invalid length byte slice provided for anchorID") +// Config defines required functions for the package Anchors +type Config interface { + GetEthereumDefaultAccountName() string + GetEthereumContextWaitTimeout() time.Duration + GetContractAddress(address string) common.Address +} + +// ToAnchorID convert the bytes into AnchorID type +// returns an error if the bytes length != AnchorIDLength +func ToAnchorID(bytes []byte) (AnchorID, error) { + var id [AnchorIDLength]byte + if !utils.IsValidByteSliceForLength(bytes, AnchorIDLength) { + return id, errors.New("invalid length byte slice provided for anchorID") } - copy(anchorBytesFixed[:], anchorBytes[:AnchorIDLength]) - return anchorBytesFixed, nil + + copy(id[:], bytes[:AnchorIDLength]) + return id, nil } +// BigInt returns anchorID in bigInt form func (a *AnchorID) BigInt() *big.Int { return utils.ByteSliceToBigInt(a[:]) } -type DocRoot [RootLength]byte +// DocumentRoot type is byte array of length DocumentRootLength +type DocumentRoot [DocumentRootLength]byte -func NewDocRoot(docRootBytes []byte) (DocRoot, error) { - var rootBytes [RootLength]byte - if !utils.IsValidByteSliceForLength(docRootBytes, RootLength) { - return rootBytes, errors.New("invalid length byte slice provided for docRoot") +// ToDocumentRoot converts bytes to DocumentRoot +// returns error if the bytes length != DocumentRootLength +func ToDocumentRoot(bytes []byte) (DocumentRoot, error) { + var root [DocumentRootLength]byte + if !utils.IsValidByteSliceForLength(bytes, DocumentRootLength) { + return root, errors.New("invalid length byte slice provided for docRoot") } - copy(rootBytes[:], docRootBytes[:RootLength]) - return rootBytes, nil -} -func NewRandomDocRoot() DocRoot { - root, _ := NewDocRoot(utils.RandomSlice(RootLength)) - return root + copy(root[:], bytes[:DocumentRootLength]) + return root, nil } -func (a DocRoot) Fixed() [RootLength]byte { - return a +// RandomDocumentRoot returns a randomly generated DocumentRoot +func RandomDocumentRoot() DocumentRoot { + root, _ := ToDocumentRoot(utils.RandomSlice(DocumentRootLength)) + return root } +// PreCommitData holds required document details for pre-commit type PreCommitData struct { AnchorID AnchorID - SigningRoot DocRoot + SigningRoot DocumentRoot CentrifugeID identity.CentID Signature []byte ExpirationBlock *big.Int SchemaVersion uint } +// CommitData holds required document details for anchoring type CommitData struct { BlockHeight uint64 AnchorID AnchorID - DocumentRoot DocRoot + DocumentRoot DocumentRoot CentrifugeID identity.CentID DocumentProofs [][DocumentProofLength]byte Signature []byte SchemaVersion uint } +// WatchCommit holds the commit data received from ethereum event type WatchCommit struct { CommitData *CommitData Error error } +// WatchPreCommit holds the pre commit data received from ethereum event type WatchPreCommit struct { PreCommit *PreCommitData Error error } -//Supported anchor schema version as stored on public repository -const AnchorSchemaVersion uint = 1 - -func SupportedSchemaVersion() uint { +// supportedSchemaVersion returns the current AnchorSchemaVersion +func supportedSchemaVersion() uint { return AnchorSchemaVersion } -func NewPreCommitData(anchorID AnchorID, signingRoot DocRoot, centrifugeID identity.CentID, signature []byte, expirationBlock *big.Int) (preCommitData *PreCommitData) { - preCommitData = &PreCommitData{} - preCommitData.AnchorID = anchorID - preCommitData.SigningRoot = signingRoot - preCommitData.CentrifugeID = centrifugeID - preCommitData.Signature = signature - preCommitData.ExpirationBlock = expirationBlock - preCommitData.SchemaVersion = SupportedSchemaVersion() - return preCommitData +// newPreCommitData returns a PreCommitData with passed in details +func newPreCommitData(anchorID AnchorID, signingRoot DocumentRoot, centrifugeID identity.CentID, signature []byte, expirationBlock *big.Int) (preCommitData *PreCommitData) { + return &PreCommitData{ + AnchorID: anchorID, + SigningRoot: signingRoot, + CentrifugeID: centrifugeID, + Signature: signature, + ExpirationBlock: expirationBlock, + SchemaVersion: supportedSchemaVersion(), + } } -func NewCommitData(blockHeight uint64, anchorID AnchorID, documentRoot DocRoot, centrifugeID identity.CentID, documentProofs [][32]byte, signature []byte) (commitData *CommitData) { +// NewCommitData returns a CommitData with passed in details +func NewCommitData(blockHeight uint64, anchorID AnchorID, documentRoot DocumentRoot, centrifugeID identity.CentID, documentProofs [][32]byte, signature []byte) (commitData *CommitData) { return &CommitData{ BlockHeight: blockHeight, AnchorID: anchorID, @@ -108,9 +137,9 @@ func NewCommitData(blockHeight uint64, anchorID AnchorID, documentRoot DocRoot, } } -func GenerateCommitHash(anchorID AnchorID, centrifugeID identity.CentID, documentRoot DocRoot) []byte { - message := append(anchorID[:], documentRoot[:]...) - message = append(message, centrifugeID[:]...) - messageToSign := crypto.Keccak256(message) - return messageToSign +// GenerateCommitHash generates Keccak256 message from AnchorID, CentID, DocumentRoot +func GenerateCommitHash(anchorID AnchorID, centrifugeID identity.CentID, documentRoot DocumentRoot) []byte { + msg := append(anchorID[:], documentRoot[:]...) + msg = append(msg, centrifugeID[:]...) + return crypto.Keccak256(msg) } diff --git a/anchors/anchor_confirmation_task.go b/anchors/anchor_confirmation_task.go index 1593ce074..6b5e52993 100644 --- a/anchors/anchor_confirmation_task.go +++ b/anchors/anchor_confirmation_task.go @@ -2,140 +2,139 @@ package anchors import ( "context" - "fmt" "math/big" "time" "github.com/centrifuge/go-centrifuge/centerrors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/gocelery" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/go-errors/errors" ) const ( - AnchorRepositoryConfirmationTaskName string = "AnchorRepositoryConfirmationTaskName" - AnchorIDParam string = "AnchorIDParam" - CentrifugeIDParam string = "CentrifugeIDParam" - BlockHeight string = "BlockHeight" - AddressParam string = "AddressParam" + anchorRepositoryConfirmationTaskName string = "anchorRepositoryConfirmationTaskName" + anchorIDParam string = "anchorIDParam" + centIDParam string = "centIDParam" + blockHeight string = "blockHeight" + addressParam string = "addressParam" ) -type AnchorCommittedWatcher interface { +type anchorCommittedWatcher interface { FilterAnchorCommitted( opts *bind.FilterOpts, from []common.Address, - anchorId []*big.Int, - centrifugeId []*big.Int) (*EthereumAnchorRepositoryContractAnchorCommittedIterator, error) + anchorID []*big.Int, + centID []*big.Int) (*EthereumAnchorRepositoryContractAnchorCommittedIterator, error) } -// AnchoringConfirmationTask is a queued task to watch ID registration events on Ethereum using EthereumAnchoryRepositoryContract. +// anchorConfirmationTask is a queued task to watch ID registration events on Ethereum using EthereumAnchoryRepositoryContract. // To see how it gets registered see bootstrapper.go and to see how it gets used see setUpRegistrationEventListener method -type AnchoringConfirmationTask struct { +type anchorConfirmationTask struct { // task parameters From common.Address AnchorID AnchorID CentrifugeID identity.CentID BlockHeight uint64 + Timeout time.Duration // state - EthContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc) + EthContextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc) EthContext context.Context - AnchorCommittedFilterer AnchorCommittedWatcher + AnchorCommittedFilterer anchorCommittedWatcher } -func NewAnchoringConfirmationTask( - anchorCommittedWatcher AnchorCommittedWatcher, - ethContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc), -) *AnchoringConfirmationTask { - return &AnchoringConfirmationTask{ - AnchorCommittedFilterer: anchorCommittedWatcher, - EthContextInitializer: ethContextInitializer, - } -} - -func (act *AnchoringConfirmationTask) Name() string { - return AnchorRepositoryConfirmationTaskName +// TaskTypeName returns anchorRepositoryConfirmationTaskName +func (act *anchorConfirmationTask) TaskTypeName() string { + return anchorRepositoryConfirmationTaskName } -func (act *AnchoringConfirmationTask) Init() error { - queue.Queue.Register(act.Name(), act) - return nil -} - -func (act *AnchoringConfirmationTask) Copy() (gocelery.CeleryTask, error) { - return &AnchoringConfirmationTask{ +// Copy returns a new instance of anchorConfirmationTask +func (act *anchorConfirmationTask) Copy() (gocelery.CeleryTask, error) { + return &anchorConfirmationTask{ act.From, act.AnchorID, act.CentrifugeID, act.BlockHeight, + act.Timeout, act.EthContextInitializer, act.EthContext, act.AnchorCommittedFilterer, }, nil } -// ParseKwargs - define a method to parse AnchorID, Address and RootHash -func (act *AnchoringConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { - anchorID, ok := kwargs[AnchorIDParam] +// ParseKwargs parses args to anchorConfirmationTask +func (act *anchorConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { + anchorID, ok := kwargs[anchorIDParam] if !ok { - return fmt.Errorf("undefined kwarg " + AnchorIDParam) + return errors.New("undefined kwarg " + anchorIDParam) } anchorIDBytes, err := getBytesAnchorID(anchorID) - if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", AnchorIDParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", anchorIDParam, err.Error()) } act.AnchorID = anchorIDBytes //parse the centrifuge id - centrifugeId, ok := kwargs[CentrifugeIDParam] + centID, ok := kwargs[centIDParam] if !ok { - return fmt.Errorf("undefined kwarg " + CentrifugeIDParam) + return errors.New("undefined kwarg " + centIDParam) } - centrifugeIdBytes, err := getBytesCentrifugeID(centrifugeId) + centIDBytes, err := getBytesCentrifugeID(centID) if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", CentrifugeIDParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", centIDParam, err.Error()) } - act.CentrifugeID = centrifugeIdBytes + act.CentrifugeID = centIDBytes // parse the address - address, ok := kwargs[AddressParam] + address, ok := kwargs[addressParam] if !ok { - return fmt.Errorf("undefined kwarg " + AddressParam) + return errors.New("undefined kwarg " + addressParam) } + addressStr, ok := address.(string) if !ok { - return fmt.Errorf("param is not hex string " + AddressParam) + return errors.New("param is not hex string " + addressParam) } + addressTyped, err := getAddressFromHexString(addressStr) if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", AddressParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", addressParam, err.Error()) } + act.From = addressTyped - if bhi, ok := kwargs[BlockHeight]; ok { + if bhi, ok := kwargs[blockHeight]; ok { bhf, ok := bhi.(float64) if ok { act.BlockHeight = uint64(bhf) } } - act.From = addressTyped + // Override default timeout param + tdRaw, ok := kwargs[queue.TimeoutParam] + if ok { + td, err := queue.GetDuration(tdRaw) + if err != nil { + return errors.New("malformed kwarg [%s] because [%s]", queue.TimeoutParam, err.Error()) + } + act.Timeout = td + } + return nil } -// RunTask calls listens to events from geth related to AnchoringConfirmationTask#AnchorID and records result. -func (act *AnchoringConfirmationTask) RunTask() (interface{}, error) { +// RunTask calls listens to events from geth related to anchorConfirmationTask#AnchorID and records result. +func (act *anchorConfirmationTask) RunTask() (interface{}, error) { log.Infof("Waiting for confirmation for the anchorID [%x]", act.AnchorID) if act.EthContext == nil { - act.EthContext, _ = act.EthContextInitializer() + act.EthContext, _ = act.EthContextInitializer(act.Timeout) } fOpts := &bind.FilterOpts{ @@ -156,17 +155,16 @@ func (act *AnchoringConfirmationTask) RunTask() (interface{}, error) { err = utils.LookForEvent(iter) if err == nil { - log.Infof("Received filtered event Anchor Confirmation for AnchorID [%x] and CentrifugeId [%s]\n", act.AnchorID.BigInt(), act.CentrifugeID.String()) + log.Infof("Received filtered event Anchor Confirmation for AnchorID [%x] and CentrifugeID [%s]\n", act.AnchorID.BigInt(), act.CentrifugeID.String()) return iter.Event, nil } - if err != utils.EventNotFound { + if err != utils.ErrEventNotFound { return nil, err } + time.Sleep(100 * time.Millisecond) } - - return nil, fmt.Errorf("failed to filter anchor events") } func getBytesAnchorID(key interface{}) (AnchorID, error) { diff --git a/anchors/anchor_confirmation_task_test.go b/anchors/anchor_confirmation_task_test.go index 306fd05d0..35f610abd 100644 --- a/anchors/anchor_confirmation_task_test.go +++ b/anchors/anchor_confirmation_task_test.go @@ -4,12 +4,13 @@ package anchors import ( "context" - "fmt" "math/big" "testing" "time" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/testingutils" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -32,83 +33,104 @@ func (m *MockAnchorCommittedFilter) FilterAnchorCommitted( } func TestAnchoringConfirmationTask_ParseKwargsHappy(t *testing.T) { - act := AnchoringConfirmationTask{} - anchorID, _ := NewAnchorID(utils.RandomSlice(AnchorIDLength)) + act := anchorConfirmationTask{} + anchorID, _ := ToAnchorID(utils.RandomSlice(AnchorIDLength)) address := common.BytesToAddress([]byte{1, 2, 3, 4}) centId, _ := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) - - kwargs, _ := utils.SimulateJsonDecodeForGocelery(map[string]interface{}{ - AnchorIDParam: anchorID, - AddressParam: address, - CentrifugeIDParam: centId, - BlockHeight: float64(0), + timeout := float64(5000) + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + anchorIDParam: anchorID, + addressParam: address, + centIDParam: centId, + blockHeight: float64(0), + queue.TimeoutParam: timeout, }) err := act.ParseKwargs(kwargs) if err != nil { - t.Fatalf("Could not parse %s or %s", AnchorIDParam, AddressParam) + assert.Nil(t, err) + t.Fatalf("Could not parse %s or %s", anchorIDParam, addressParam) } //convert byte 32 to big int assert.Equal(t, anchorID, anchorID, "Resulting anchor Id should have the same ID as the input") assert.Equal(t, address, act.From, "Resulting address should have the same ID as the input") assert.Equal(t, centId, act.CentrifugeID, "Resulting centId should have the same centId as the input") + assert.Equal(t, time.Duration(timeout), act.Timeout, "Resulting timeout should have the same timeout as the input") } func TestAnchoringConfirmationTask_ParseKwargsAnchorNotPassed(t *testing.T) { - act := AnchoringConfirmationTask{} + act := anchorConfirmationTask{} address := common.BytesToAddress([]byte{1, 2, 3, 4}) var centrifugeIdBytes [6]byte - kwargs, _ := utils.SimulateJsonDecodeForGocelery(map[string]interface{}{ - AddressParam: address, - CentrifugeIDParam: centrifugeIdBytes, + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + addressParam: address, + centIDParam: centrifugeIdBytes, }) err := act.ParseKwargs(kwargs) assert.NotNil(t, err, "Anchor id should not have been parsed") } func TestAnchoringConfirmationTask_ParseKwargsInvalidAnchor(t *testing.T) { - act := AnchoringConfirmationTask{} + act := anchorConfirmationTask{} anchorID := 123 address := common.BytesToAddress([]byte{1, 2, 3, 4}) - kwargs, _ := utils.SimulateJsonDecodeForGocelery(map[string]interface{}{ - AnchorIDParam: anchorID, - AddressParam: address, + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + anchorIDParam: anchorID, + addressParam: address, }) err := act.ParseKwargs(kwargs) assert.NotNil(t, err, "Anchor id should not have been parsed because it was of incorrect type") } func TestAnchoringConfirmationTask_ParseKwargsAddressNotPassed(t *testing.T) { - act := AnchoringConfirmationTask{} + act := anchorConfirmationTask{} anchorID := [32]byte{1, 2, 3} - kwargs, _ := utils.SimulateJsonDecodeForGocelery(map[string]interface{}{ - AnchorIDParam: anchorID, + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + anchorIDParam: anchorID, }) err := act.ParseKwargs(kwargs) assert.NotNil(t, err, "address should not have been parsed") } func TestAnchoringConfirmationTask_ParseKwargsInvalidAddress(t *testing.T) { - act := AnchoringConfirmationTask{} + act := anchorConfirmationTask{} anchorID := [32]byte{1, 2, 3} address := 123 - kwargs, _ := utils.SimulateJsonDecodeForGocelery(map[string]interface{}{ - AnchorIDParam: anchorID, - AddressParam: address, + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + anchorIDParam: anchorID, + addressParam: address, }) err := act.ParseKwargs(kwargs) assert.NotNil(t, err, "address should not have been parsed because it was of incorrect type") } +func TestAnchoringConfirmationTask_ParseKwargsInvalidTimeout(t *testing.T) { + act := anchorConfirmationTask{} + anchorID, _ := ToAnchorID(utils.RandomSlice(AnchorIDLength)) + address := common.BytesToAddress([]byte{1, 2, 3, 4}) + + centId, _ := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) + timeout := "int64" + kwargs, _ := utils.SimulateJSONDecodeForGocelery(map[string]interface{}{ + anchorIDParam: anchorID, + addressParam: address, + centIDParam: centId, + blockHeight: float64(0), + queue.TimeoutParam: timeout, + }) + err := act.ParseKwargs(kwargs) + assert.NotNil(t, err, "timeout should not have been parsed because it was of incorrect type") +} + func TestAnchoringConfirmationTask_RunTaskIterError(t *testing.T) { anchorID := [32]byte{1, 2, 3} address := common.BytesToAddress([]byte{1, 2, 3, 4}) - act := AnchoringConfirmationTask{ + act := anchorConfirmationTask{ AnchorID: anchorID, From: address, - AnchorCommittedFilterer: &MockAnchorCommittedFilter{err: fmt.Errorf("failed iterator")}, + AnchorCommittedFilterer: &MockAnchorCommittedFilter{err: errors.New("failed iterator")}, EthContext: context.Background(), } @@ -122,11 +144,11 @@ func TestAnchoringConfirmationTask_RunTaskWatchError(t *testing.T) { ctx, _ := context.WithDeadline(context.Background(), toBeDone) anchorID := [32]byte{1, 2, 3} address := common.BytesToAddress([]byte{1, 2, 3, 4}) - act := AnchoringConfirmationTask{ + act := anchorConfirmationTask{ AnchorID: anchorID, From: address, AnchorCommittedFilterer: &MockAnchorCommittedFilter{iter: &EthereumAnchorRepositoryContractAnchorCommittedIterator{ - fail: fmt.Errorf("watch error"), + fail: errors.New("watch error"), sub: &testingutils.MockSubscription{}, }}, EthContext: ctx, diff --git a/anchors/anchor_repository.go b/anchors/anchor_repository.go index 594a80fd4..ff1f9b176 100644 --- a/anchors/anchor_repository.go +++ b/anchors/anchor_repository.go @@ -10,52 +10,9 @@ import ( 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 +// implemented by any type that stores and retrieves the anchoring, and pre anchoring details. type AnchorRepository interface { - PreCommitAnchor(anchorID AnchorID, signingRoot DocRoot, centrifugeID identity.CentID, signature []byte, expirationBlock *big.Int) (<-chan *WatchPreCommit, error) - CommitAnchor(anchorID AnchorID, documentRoot DocRoot, centrifugeId identity.CentID, documentProofs [][32]byte, signature []byte) (<-chan *WatchCommit, error) - GetDocumentRootOf(anchorID AnchorID) (DocRoot, error) -} - -// PreCommitAnchor initiates the PreCommit call on the smart contract -// with passed in variables and returns a channel for transaction confirmation -func PreCommitAnchor(anchorID AnchorID, signingRoot DocRoot, centrifugeId identity.CentID, signature []byte, expirationBlock *big.Int) (<-chan *WatchPreCommit, error) { - anchorRepository, _ := getConfiguredRepository() - - confirmations, err := anchorRepository.PreCommitAnchor(anchorID, signingRoot, centrifugeId, signature, expirationBlock) - if err != nil { - log.Errorf("Failed to pre-commit the anchor [id:%x, hash:%x ]: %v", anchorID, signingRoot, err) - } - return confirmations, err -} - -// CommitAnchor initiates the Commit call on smart contract -// with passed in variables and returns a channel for transaction confirmation -func CommitAnchor(anchorID AnchorID, documentRoot DocRoot, centrifugeID identity.CentID, documentProofs [][32]byte, signature []byte) (<-chan *WatchCommit, error) { - anchorRepository, _ := getConfiguredRepository() - - confirmations, err := anchorRepository.CommitAnchor(anchorID, documentRoot, centrifugeID, documentProofs, signature) - if err != nil { - log.Errorf("Failed to commit the anchor [id:%x, hash:%x ]: %v", anchorID, documentRoot, err) - } - return confirmations, err -} - -// anchorRepository is a singleton to keep track of the anchorRepository -var anchorRepository AnchorRepository - -// setAnchorRepository sets the passed in repository as default one -func setAnchorRepository(ar AnchorRepository) { - anchorRepository = ar -} - -// GetAnchorRepository returns default anchor repository -func GetAnchorRepository() AnchorRepository { - return anchorRepository -} - -// getConfiguredRepository will later pull a configured repository (if not only using Ethereum as the anchor repository) -// For now hard-coded to the Ethereum setup -func getConfiguredRepository() (AnchorRepository, error) { - return anchorRepository, nil + PreCommitAnchor(anchorID AnchorID, signingRoot DocumentRoot, centID identity.CentID, signature []byte, expirationBlock *big.Int) (<-chan *WatchPreCommit, error) + CommitAnchor(anchorID AnchorID, documentRoot DocumentRoot, centID identity.CentID, documentProofs [][32]byte, signature []byte) (<-chan *WatchCommit, error) + GetDocumentRootOf(anchorID AnchorID) (DocumentRoot, error) } diff --git a/anchors/anchor_repository_integration_test.go b/anchors/anchor_repository_integration_test.go index 9bd2167e4..db9b29c23 100644 --- a/anchors/anchor_repository_integration_test.go +++ b/anchors/anchor_repository_integration_test.go @@ -17,34 +17,30 @@ import ( "github.com/stretchr/testify/assert" ) -var identityService identity.Service - -// Add Key -var testAddress string -var testPrivateKey string +var ( + identityService identity.Service + anchorRepo anchors.AnchorRepository +) func TestMain(m *testing.M) { - cc.TestFunctionalEthereumBootstrap() - identityService = identity.IDService + ctx := cc.TestFunctionalEthereumBootstrap() + anchorRepo = ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) + identityService = ctx[identity.BootstrappedIDService].(identity.Service) result := m.Run() cc.TestFunctionalEthereumTearDown() os.Exit(result) } func createIdentityWithKeys(t *testing.T, centrifugeId []byte) []byte { - centIdTyped, _ := identity.ToCentID(centrifugeId) id, confirmations, err := identityService.CreateIdentity(centIdTyped) assert.Nil(t, err, "should not error out when creating identity") - watchRegisteredIdentity := <-confirmations assert.Nil(t, watchRegisteredIdentity.Error, "No error thrown by context") - // LookupIdentityForId id, err = identityService.LookupIdentityForID(centIdTyped) assert.Nil(t, err, "should not error out when resolving identity") - testPrivateKey = "0x17e063fa17dd8274b09c14b253697d9a20afff74ace3c04fdb1b9c814ce0ada5" pubKey, _ := hexutil.Decode("0xc8dd3d66e112fae5c88fe6a677be24013e53c33e") confirmations, err = id.AddKeyToIdentity(context.Background(), identity.KeyPurposeEthMsgAuth, pubKey) @@ -62,23 +58,23 @@ func TestCommitAnchor_Integration(t *testing.T) { centrifugeId := utils.RandomSlice(identity.CentIDLength) createIdentityWithKeys(t, centrifugeId) testPrivateKey, _ := hexutil.Decode("0x17e063fa17dd8274b09c14b253697d9a20afff74ace3c04fdb1b9c814ce0ada5") - anchorIDTyped, _ := anchors.NewAnchorID(anchorID) + anchorIDTyped, _ := anchors.ToAnchorID(anchorID) centIdTyped, _ := identity.ToCentID(centrifugeId) - docRootTyped, _ := anchors.NewDocRoot(documentRoot) + docRootTyped, _ := anchors.ToDocumentRoot(documentRoot) messageToSign := anchors.GenerateCommitHash(anchorIDTyped, centIdTyped, docRootTyped) signature, _ := secp256k1.SignEthereum(messageToSign, testPrivateKey) commitAnchor(t, anchorID, centrifugeId, documentRoot, signature, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}) - gotDocRoot, err := anchors.GetAnchorRepository().GetDocumentRootOf(anchorIDTyped) + gotDocRoot, err := anchorRepo.GetDocumentRootOf(anchorIDTyped) assert.Nil(t, err) assert.Equal(t, docRootTyped, gotDocRoot) } func commitAnchor(t *testing.T, anchorID, centrifugeId, documentRoot, signature []byte, documentProofs [][32]byte) { - anchorIDTyped, _ := anchors.NewAnchorID(anchorID) - docRootTyped, _ := anchors.NewDocRoot(documentRoot) + anchorIDTyped, _ := anchors.ToAnchorID(anchorID) + docRootTyped, _ := anchors.ToDocumentRoot(documentRoot) centIdFixed, _ := identity.ToCentID(centrifugeId) - confirmations, err := anchors.CommitAnchor(anchorIDTyped, docRootTyped, centIdFixed, documentProofs, signature) + confirmations, err := anchorRepo.CommitAnchor(anchorIDTyped, docRootTyped, centIdFixed, documentProofs, signature) if err != nil { t.Fatalf("Error commit Anchor %v", err) } @@ -103,10 +99,10 @@ func TestCommitAnchor_Integration_Concurrent(t *testing.T) { messageToSign := anchors.GenerateCommitHash(currentAnchorId, centIdFixed, currentDocumentRoot) signature, _ := secp256k1.SignEthereum(messageToSign, testPrivateKey) documentProofs := [][anchors.DocumentProofLength]byte{utils.RandomByte32()} - h, err := ethereum.GetConnection().GetClient().HeaderByNumber(context.Background(), nil) + h, err := ethereum.GetClient().GetEthClient().HeaderByNumber(context.Background(), nil) assert.Nil(t, err, " error must be nil") commitDataList[ix] = anchors.NewCommitData(h.Number.Uint64(), currentAnchorId, currentDocumentRoot, centIdFixed, documentProofs, signature) - confirmationList[ix], err = anchors.CommitAnchor(currentAnchorId, currentDocumentRoot, centIdFixed, documentProofs, signature) + confirmationList[ix], err = anchorRepo.CommitAnchor(currentAnchorId, currentDocumentRoot, centIdFixed, documentProofs, signature) if err != nil { t.Fatalf("Error commit Anchor %v", err) } @@ -120,7 +116,7 @@ func TestCommitAnchor_Integration_Concurrent(t *testing.T) { assert.Equal(t, commitDataList[ix].DocumentRoot, watchSingleAnchor.CommitData.DocumentRoot, "Should have the document root that was passed into create function [%v]", watchSingleAnchor.CommitData.DocumentRoot) anchorID := commitDataList[ix].AnchorID docRoot := commitDataList[ix].DocumentRoot - gotDocRoot, err := anchors.GetAnchorRepository().GetDocumentRootOf(anchorID) + gotDocRoot, err := anchorRepo.GetDocumentRootOf(anchorID) assert.Nil(t, err) assert.Equal(t, docRoot, gotDocRoot) } diff --git a/anchors/anchor_test.go b/anchors/anchor_test.go index 9c0253453..f600210f9 100644 --- a/anchors/anchor_test.go +++ b/anchors/anchor_test.go @@ -12,11 +12,15 @@ import ( "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg Config + func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &config.Bootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(Config) result := m.Run() bootstrap.RunTestTeardown(ibootstappers) os.Exit(result) @@ -46,7 +50,7 @@ func TestNewAnchorId(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := NewAnchorID(test.slice) + _, err := ToAnchorID(test.slice) assert.Equal(t, test.err, err.Error()) }) } @@ -60,12 +64,12 @@ func TestNewDocRoot(t *testing.T) { }{ { "smallerSlice", - utils.RandomSlice(RootLength - 1), + utils.RandomSlice(DocumentRootLength - 1), "invalid length byte slice provided for docRoot", }, { "largerSlice", - utils.RandomSlice(RootLength + 1), + utils.RandomSlice(DocumentRootLength + 1), "invalid length byte slice provided for docRoot", }, { @@ -76,7 +80,7 @@ func TestNewDocRoot(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := NewDocRoot(test.slice) + _, err := ToDocumentRoot(test.slice) assert.Equal(t, test.err, err.Error()) }) } diff --git a/anchors/bootstrapper.go b/anchors/bootstrapper.go index 5463c86e1..6a9e55446 100644 --- a/anchors/bootstrapper.go +++ b/anchors/bootstrapper.go @@ -1,37 +1,52 @@ package anchors import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/queue" ) -type Bootstrapper struct { -} +// BootstrappedAnchorRepo is used as a key to map the configured anchor repository through context. +const BootstrappedAnchorRepo string = "BootstrappedAnchorRepo" + +// Bootstrapper implements bootstrapper.Bootstrapper for package requirement initialisations. +type Bootstrapper struct{} -// Bootstrap initializes the AnchorRepositoryContract as well as the AnchoringConfirmationTask that depends on it. -// the AnchoringConfirmationTask is added to be registered on the Queue at queue.Bootstrapper -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - if _, ok := context[bootstrap.BootstrappedConfig]; !ok { +// Bootstrap initializes the anchorRepositoryContract as well as the anchorConfirmationTask that depends on it. +// the anchorConfirmationTask is added to be registered on the Queue at queue.Bootstrapper. +func (Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + if _, ok := ctx[bootstrap.BootstrappedConfig]; !ok { return errors.New("config hasn't been initialized") } - if _, ok := context[bootstrap.BootstrappedEthereumClient]; !ok { + cfg := ctx[bootstrap.BootstrappedConfig].(Config) + + if _, ok := ctx[ethereum.BootstrappedEthereumClient]; !ok { return errors.New("ethereum client hasn't been initialized") } + client := ctx[ethereum.BootstrappedEthereumClient].(ethereum.Client) - client := ethereum.GetConnection() - repositoryContract, err := NewEthereumAnchorRepositoryContract(config.Config.GetContractAddress("anchorRepository"), client.GetClient()) + repositoryContract, err := NewEthereumAnchorRepositoryContract(cfg.GetContractAddress("anchorRepository"), client.GetEthClient()) if err != nil { return err } - anchorRepo := NewEthereumAnchorRepository(config.Config, repositoryContract) - setAnchorRepository(anchorRepo) - if err != nil { - return err + if _, ok := ctx[bootstrap.BootstrappedQueueServer]; !ok { + return errors.New("queue server hasn't been initialized") } - return queue.InstallQueuedTask(context, NewAnchoringConfirmationTask(&repositoryContract.EthereumAnchorRepositoryContractFilterer, ethereum.DefaultWaitForTransactionMiningContext)) + + queueSrv := ctx[bootstrap.BootstrappedQueueServer].(*queue.Server) + repo := newEthereumAnchorRepository(cfg, repositoryContract, queueSrv, ethereum.GetClient) + ctx[BootstrappedAnchorRepo] = repo + + task := &anchorConfirmationTask{ + // Passing timeout as a common property for every request, if we need more fine-grain control per request then we will override by invoker + Timeout: cfg.GetEthereumContextWaitTimeout(), + AnchorCommittedFilterer: &repositoryContract.EthereumAnchorRepositoryContractFilterer, + EthContextInitializer: ethereum.DefaultWaitForTransactionMiningContext, + } + + queueSrv.RegisterTaskType(task.TaskTypeName(), task) + return nil } diff --git a/anchors/ethereum_anchor_repository.go b/anchors/ethereum_anchor_repository.go index c126ca798..ab2defb0a 100644 --- a/anchors/ethereum_anchor_repository.go +++ b/anchors/ethereum_anchor_repository.go @@ -4,94 +4,90 @@ import ( "context" "math/big" + "time" + + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/queue" - "github.com/centrifuge/gocelery" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" - "github.com/go-errors/errors" ) -type Config interface { - GetEthereumDefaultAccountName() string -} - -type AnchorRepositoryContract interface { - //transactions - PreCommit(opts *bind.TransactOpts, anchorID *big.Int, signingRoot [32]byte, centrifugeId *big.Int, signature []byte, expirationBlock *big.Int) (*types.Transaction, error) - Commit(opts *bind.TransactOpts, _anchorID *big.Int, _documentRoot [32]byte, _centrifugeId *big.Int, _documentProofs [][32]byte, _signature []byte) (*types.Transaction, error) +type anchorRepositoryContract interface { + PreCommit(opts *bind.TransactOpts, anchorID *big.Int, signingRoot [32]byte, centID *big.Int, signature []byte, expirationBlock *big.Int) (*types.Transaction, error) + Commit(opts *bind.TransactOpts, anchorID *big.Int, documentRoot [32]byte, centID *big.Int, documentProofs [][32]byte, signatures []byte) (*types.Transaction, error) Commits(opts *bind.CallOpts, anchorID *big.Int) (docRoot [32]byte, err error) } -type WatchAnchorPreCommitted interface { + +type watchAnchorPreCommitted interface { //event name: AnchorPreCommitted WatchAnchorPreCommitted(opts *bind.WatchOpts, sink chan<- *EthereumAnchorRepositoryContractAnchorPreCommitted, from []common.Address, anchorID []*big.Int) (event.Subscription, error) } -type WatchAnchorCommitted interface { - //event name: AnchorCommitted - WatchAnchorCommitted(opts *bind.WatchOpts, sink chan<- *EthereumAnchorRepositoryContractAnchorCommitted, - from []common.Address, anchorID []*big.Int, centrifugeId []*big.Int) (event.Subscription, error) -} - -type EthereumAnchorRepository struct { +type ethereumAnchorRepository struct { config Config - anchorRepositoryContract AnchorRepositoryContract + anchorRepositoryContract anchorRepositoryContract + gethClientFinder func() ethereum.Client + queue *queue.Server } -func NewEthereumAnchorRepository(config Config, anchorRepositoryContract AnchorRepositoryContract) *EthereumAnchorRepository { - return &EthereumAnchorRepository{config: config, anchorRepositoryContract: anchorRepositoryContract} +func newEthereumAnchorRepository(config Config, anchorRepositoryContract anchorRepositoryContract, queue *queue.Server, gethClientFinder func() ethereum.Client) AnchorRepository { + return ðereumAnchorRepository{config: config, anchorRepositoryContract: anchorRepositoryContract, gethClientFinder: gethClientFinder, queue: queue} } -// Commits takes an anchorID and returns the corresponding documentRoot from the chain -func (ethRepository *EthereumAnchorRepository) GetDocumentRootOf(anchorID AnchorID) (docRoot DocRoot, err error) { +// GetDocumentRootOf takes an anchorID and returns the corresponding documentRoot from the chain. +func (ethRepository *ethereumAnchorRepository) GetDocumentRootOf(anchorID AnchorID) (docRoot DocumentRoot, err error) { // Ignoring cancelFunc as code will block until response or timeout is triggered - opts, _ := ethereum.GetGethCallOpts() + opts, _ := ethRepository.gethClientFinder().GetGethCallOpts() return ethRepository.anchorRepositoryContract.Commits(opts, anchorID.BigInt()) } -//PreCommitAnchor will call the transaction PreCommit on the smart contract -func (ethRepository *EthereumAnchorRepository) PreCommitAnchor(anchorID AnchorID, signingRoot DocRoot, centrifugeId identity.CentID, signature []byte, expirationBlock *big.Int) (confirmations <-chan *WatchPreCommit, err error) { +// PreCommitAnchor will call the transaction PreCommit on the smart contract +func (ethRepository *ethereumAnchorRepository) PreCommitAnchor(anchorID AnchorID, signingRoot DocumentRoot, centID identity.CentID, signature []byte, expirationBlock *big.Int) (confirmations <-chan *WatchPreCommit, err error) { ethRepositoryContract := ethRepository.anchorRepositoryContract - opts, err := ethereum.GetConnection().GetTxOpts(ethRepository.config.GetEthereumDefaultAccountName()) + opts, err := ethereum.GetClient().GetTxOpts(ethRepository.config.GetEthereumDefaultAccountName()) if err != nil { - return + return confirmations, err } - preCommitData := NewPreCommitData(anchorID, signingRoot, centrifugeId, signature, expirationBlock) + + preCommitData := newPreCommitData(anchorID, signingRoot, centID, signature, expirationBlock) if err != nil { - return + return confirmations, err } err = sendPreCommitTransaction(ethRepositoryContract, opts, preCommitData) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Errorf("Failed to send Ethereum pre-commit transaction [id: %x, signingRoot: %x, SchemaVersion:%v]: %v", preCommitData.AnchorID, preCommitData.SigningRoot, preCommitData.SchemaVersion, wError) - return + return confirmations, err } + return confirmations, err } -// CommitAnchor will send a commit transaction to ethereum -func (ethRepository *EthereumAnchorRepository) CommitAnchor(anchorID AnchorID, documentRoot DocRoot, centrifugeId identity.CentID, documentProofs [][32]byte, signature []byte) (confirmations <-chan *WatchCommit, err error) { - conn := ethereum.GetConnection() +// CommitAnchor will send a commit transaction to Ethereum. +func (ethRepository *ethereumAnchorRepository) CommitAnchor(anchorID AnchorID, documentRoot DocumentRoot, centID identity.CentID, documentProofs [][32]byte, signature []byte) (confirmations <-chan *WatchCommit, err error) { + conn := ethereum.GetClient() opts, err := conn.GetTxOpts(ethRepository.config.GetEthereumDefaultAccountName()) if err != nil { return nil, err } - h, err := conn.GetClient().HeaderByNumber(context.Background(), nil) + h, err := conn.GetEthClient().HeaderByNumber(context.Background(), nil) if err != nil { return nil, err } - cd := NewCommitData(h.Number.Uint64(), anchorID, documentRoot, centrifugeId, documentProofs, signature) - confirmations, err = setUpCommitEventListener(opts.From, cd) + cd := NewCommitData(h.Number.Uint64(), anchorID, documentRoot, centID, documentProofs, signature) + confirmations, err = ethRepository.setUpCommitEventListener(ethRepository.config.GetEthereumContextWaitTimeout(), opts.From, cd) + if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Errorf("Failed to set up event listener for commit transaction [id: %x, hash: %x]: %v", cd.AnchorID, cd.DocumentRoot, wError) return @@ -99,7 +95,7 @@ func (ethRepository *EthereumAnchorRepository) CommitAnchor(anchorID AnchorID, d err = sendCommitTransaction(ethRepository.anchorRepositoryContract, opts, cd) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Errorf("Failed to send Ethereum commit transaction[id: %x, hash: %x, SchemaVersion:%v]: %v", cd.AnchorID, cd.DocumentRoot, cd.SchemaVersion, wError) return @@ -107,45 +103,45 @@ func (ethRepository *EthereumAnchorRepository) CommitAnchor(anchorID AnchorID, d return confirmations, err } -// sendPreCommitTransaction sends the actual transaction to the ethereum node -func sendPreCommitTransaction(contract AnchorRepositoryContract, opts *bind.TransactOpts, preCommitData *PreCommitData) (err error) { +// sendPreCommitTransaction sends the actual transaction to the ethereum node. +func sendPreCommitTransaction(contract anchorRepositoryContract, opts *bind.TransactOpts, preCommitData *PreCommitData) error { //preparation of data in specific types for the call to Ethereum schemaVersion := big.NewInt(int64(preCommitData.SchemaVersion)) - tx, err := ethereum.GetConnection().SubmitTransactionWithRetries(contract.PreCommit, opts, preCommitData.AnchorID, preCommitData.SigningRoot, + tx, err := ethereum.GetClient().SubmitTransactionWithRetries(contract.PreCommit, opts, preCommitData.AnchorID, preCommitData.SigningRoot, preCommitData.CentrifugeID, preCommitData.Signature, preCommitData.ExpirationBlock, schemaVersion) if err != nil { - return - } else { - log.Infof("Sent off transaction pre-commit [id: %x, hash: %x, SchemaVersion:%v] to registry. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", preCommitData.AnchorID, - preCommitData.SigningRoot, schemaVersion, tx.Hash(), tx.Nonce(), tx.CheckNonce()) + return err } + log.Infof("Sent off transaction pre-commit [id: %x, hash: %x, SchemaVersion:%v] to registry. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", preCommitData.AnchorID, + preCommitData.SigningRoot, schemaVersion, tx.Hash(), tx.Nonce(), tx.CheckNonce()) + log.Infof("Transfer pending: 0x%x\n", tx.Hash()) - return + return nil } // sendCommitTransaction sends the actual transaction to register the Anchor on Ethereum registry contract -func sendCommitTransaction(contract AnchorRepositoryContract, opts *bind.TransactOpts, commitData *CommitData) (err error) { - tx, err := ethereum.GetConnection().SubmitTransactionWithRetries(contract.Commit, opts, commitData.AnchorID.BigInt(), commitData.DocumentRoot, +func sendCommitTransaction(contract anchorRepositoryContract, opts *bind.TransactOpts, commitData *CommitData) error { + tx, err := ethereum.GetClient().SubmitTransactionWithRetries(contract.Commit, opts, commitData.AnchorID.BigInt(), commitData.DocumentRoot, commitData.CentrifugeID.BigInt(), commitData.DocumentProofs, commitData.Signature) if err != nil { return err - } else { - log.Infof("Sent off the anchor [id: %x, hash: %x] to registry. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", commitData.AnchorID, - commitData.DocumentRoot, tx.Hash(), tx.Nonce(), tx.CheckNonce()) } + log.Infof("Sent off the anchor [id: %x, hash: %x] to registry. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", commitData.AnchorID, + commitData.DocumentRoot, tx.Hash(), tx.Nonce(), tx.CheckNonce()) log.Infof("Transfer pending: 0x%x\n", tx.Hash()) - return + return nil } +// TODO: This method is only used by setUpPreCommitEventListener below, it will be changed soon so we can remove the hardcoded `time.Second` and use the global one func generateEventContext() (*bind.WatchOpts, context.CancelFunc) { //listen to this particular anchor being mined/event is triggered - ctx, cancelFunc := ethereum.DefaultWaitForTransactionMiningContext() + ctx, cancelFunc := ethereum.DefaultWaitForTransactionMiningContext(time.Second) watchOpts := &bind.WatchOpts{Context: ctx} return watchOpts, cancelFunc @@ -154,20 +150,20 @@ func generateEventContext() (*bind.WatchOpts, context.CancelFunc) { // setUpPreCommitEventListener sets up the listened for the "PreCommit" event to notify the upstream code // about successful mining/creation of a pre-commit. -func setUpPreCommitEventListener(contractEvent WatchAnchorPreCommitted, from common.Address, preCommitData *PreCommitData) (confirmations chan *WatchPreCommit, err error) { +func setUpPreCommitEventListener(contractEvent watchAnchorPreCommitted, from common.Address, preCommitData *PreCommitData) (confirmations chan *WatchPreCommit, err error) { watchOpts, cancelFunc := generateEventContext() //there should always be only one notification coming for this //single anchor being registered anchorPreCommittedEvents := make(chan *EthereumAnchorRepositoryContractAnchorPreCommitted) confirmations = make(chan *WatchPreCommit) - go waitAndRoutePreCommitEvent(anchorPreCommittedEvents, watchOpts.Context, confirmations, preCommitData) + go waitAndRoutePreCommitEvent(watchOpts.Context, anchorPreCommittedEvents, confirmations, preCommitData) // Somehow there are some possible resource leakage situations with this handling but I have to understand // Subscriptions a bit better before writing this code. _, err = contractEvent.WatchAnchorPreCommitted(watchOpts, anchorPreCommittedEvents, []common.Address{from}, []*big.Int{preCommitData.AnchorID.BigInt()}) if err != nil { - wError := errors.WrapPrefix(err, "Could not subscribe to event logs for anchor registration", 1) + wError := errors.New("Could not subscribe to event logs for anchor registration: %v", err) log.Errorf("Failed to watch anchor registered event: %v", wError.Error()) cancelFunc() // cancel the event router return confirmations, wError @@ -177,24 +173,24 @@ func setUpPreCommitEventListener(contractEvent WatchAnchorPreCommitted, from com // setUpCommitEventListener sets up the listened for the "AnchorCommitted" event to notify the upstream code // about successful mining/creation of a commit -func setUpCommitEventListener(from common.Address, commitData *CommitData) (confirmations chan *WatchCommit, err error) { +func (ethRepository *ethereumAnchorRepository) setUpCommitEventListener(timeout time.Duration, from common.Address, commitData *CommitData) (confirmations chan *WatchCommit, err error) { confirmations = make(chan *WatchCommit) - asyncRes, err := queue.Queue.DelayKwargs(AnchorRepositoryConfirmationTaskName, map[string]interface{}{ - AnchorIDParam: commitData.AnchorID, - AddressParam: from, - CentrifugeIDParam: commitData.CentrifugeID, - BlockHeight: commitData.BlockHeight, + asyncRes, err := ethRepository.queue.EnqueueJob(anchorRepositoryConfirmationTaskName, map[string]interface{}{ + anchorIDParam: commitData.AnchorID, + addressParam: from, + centIDParam: commitData.CentrifugeID, + blockHeight: commitData.BlockHeight, }) if err != nil { return nil, err } - go waitAndRouteCommitEvent(asyncRes, confirmations, commitData) + go waitAndRouteCommitEvent(timeout, asyncRes, confirmations, commitData) return confirmations, nil } // waitAndRoutePreCommitEvent notifies the confirmations channel whenever a pre-commit is being noted as Ethereum event -func waitAndRoutePreCommitEvent(conf <-chan *EthereumAnchorRepositoryContractAnchorPreCommitted, ctx context.Context, confirmations chan<- *WatchPreCommit, preCommitData *PreCommitData) { +func waitAndRoutePreCommitEvent(ctx context.Context, conf <-chan *EthereumAnchorRepositoryContractAnchorPreCommitted, confirmations chan<- *WatchPreCommit, preCommitData *PreCommitData) { for { select { case <-ctx.Done(): @@ -210,7 +206,7 @@ func waitAndRoutePreCommitEvent(conf <-chan *EthereumAnchorRepositoryContractAnc } // waitAndRouteCommitEvent notifies the confirmations channel whenever a commit is being noted as Ethereum event -func waitAndRouteCommitEvent(asyncResult *gocelery.AsyncResult, confirmations chan<- *WatchCommit, commitData *CommitData) { - _, err := asyncResult.Get(ethereum.GetDefaultContextTimeout()) +func waitAndRouteCommitEvent(timeout time.Duration, asyncResult queue.TaskResult, confirmations chan<- *WatchCommit, commitData *CommitData) { + _, err := asyncResult.Get(timeout) confirmations <- &WatchCommit{commitData, err} } diff --git a/anchors/ethereum_anchor_repository_test.go b/anchors/ethereum_anchor_repository_test.go index 78e40ece5..20d81a7e1 100644 --- a/anchors/ethereum_anchor_repository_test.go +++ b/anchors/ethereum_anchor_repository_test.go @@ -6,9 +6,10 @@ import ( "math/big" "testing" - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/keytools/secp256k1" + "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" @@ -18,7 +19,7 @@ import ( type mockAnchorRepo struct { mock.Mock - AnchorRepositoryContract + anchorRepositoryContract } func (m *mockAnchorRepo) Commits(opts *bind.CallOpts, anchorID *big.Int) (docRoot [32]byte, err error) { @@ -35,9 +36,9 @@ func TestCorrectCommitSignatureGen(t *testing.T) { correctCommitToSign := "0x15f9cb57608a7ef31428fd6b1cb7ea2002ab032211d882b920c1474334004d6b" correctCommitSignature := "0xb4051d6d03c3bf39f4ec4ba949a91a358b0cacb4804b82ed2ba978d338f5e747770c00b63c8e50c1a7aa5ba629870b54c2068a56f8b43460aa47891c6635d36d01" testPrivateKey, _ := hexutil.Decode("0x17e063fa17dd8274b09c14b253697d9a20afff74ace3c04fdb1b9c814ce0ada5") - anchorIDTyped, _ := NewAnchorID(anchorID) + anchorIDTyped, _ := ToAnchorID(anchorID) centIdTyped, _ := identity.ToCentID(centrifugeId) - docRootTyped, _ := NewDocRoot(documentRoot) + docRootTyped, _ := ToDocumentRoot(documentRoot) messageToSign := GenerateCommitHash(anchorIDTyped, centIdTyped, docRootTyped) assert.Equal(t, correctCommitToSign, hexutil.Encode(messageToSign), "messageToSign not calculated correctly") signature, _ := secp256k1.SignEthereum(messageToSign, testPrivateKey) @@ -62,8 +63,8 @@ func TestGenerateAnchor(t *testing.T) { commitData := NewCommitData(0, currentAnchorID, documentRoot32Bytes, centIdTyped, documentProofs, signature) - anchorID, _ := NewAnchorID(currentAnchorID[:]) - docRoot, _ := NewDocRoot(documentRoot32Bytes[:]) + anchorID, _ := ToAnchorID(currentAnchorID[:]) + docRoot, _ := ToDocumentRoot(documentRoot32Bytes[:]) assert.Equal(t, commitData.AnchorID, anchorID, "Anchor should have the passed ID") assert.Equal(t, commitData.DocumentRoot, docRoot, "Anchor should have the passed document root") @@ -74,10 +75,14 @@ func TestGenerateAnchor(t *testing.T) { func TestGetDocumentRootOf(t *testing.T) { repo := &mockAnchorRepo{} - anchorID, err := NewAnchorID(utils.RandomSlice(32)) + anchorID, err := ToAnchorID(utils.RandomSlice(32)) assert.Nil(t, err) - ethRepo := NewEthereumAnchorRepository(config.Config, repo) + ethClient := &testingcommons.MockEthClient{} + ethClient.On("GetGethCallOpts").Return(nil) + ethRepo := newEthereumAnchorRepository(cfg, repo, nil, func() ethereum.Client { + return ethClient + }) docRoot := utils.RandomByte32() repo.On("Commits", mock.Anything, mock.Anything).Return(docRoot, nil) gotRoot, err := ethRepo.GetDocumentRootOf(anchorID) diff --git a/anchors/test_bootstrapper.go b/anchors/test_bootstrapper.go index 85becb065..e96c2ee28 100644 --- a/anchors/test_bootstrapper.go +++ b/anchors/test_bootstrapper.go @@ -3,25 +3,21 @@ package anchors import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/bootstrap" ) -func (b *Bootstrapper) TestBootstrap(context map[string]interface{}) error { +const BootstrappedAnchorRepository string = "BootstrappedAnchorRepository" + +func (b Bootstrapper) TestBootstrap(context map[string]interface{}) error { if _, ok := context[bootstrap.BootstrappedConfig]; !ok { return errors.New("config hasn't been initialized") } - if repo, ok := context[bootstrap.BootstrappedAnchorRepository]; ok { - setAnchorRepository(repo.(AnchorRepository)) - } else { - b.Bootstrap(context) - } - - return nil + return b.Bootstrap(context) } -func (b *Bootstrapper) TestTearDown() error { +func (b Bootstrapper) TestTearDown() error { return nil } diff --git a/api/bootstrapper.go b/api/bootstrapper.go new file mode 100644 index 000000000..1028555af --- /dev/null +++ b/api/bootstrapper.go @@ -0,0 +1,28 @@ +package api + +import ( + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" +) + +// Bootstrapper implements bootstrapper.Bootstrapper +type Bootstrapper struct{} + +// Bootstrap initiates api server +func (b Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + cfg, ok := ctx[bootstrap.BootstrappedConfig].(Config) + if !ok { + return errors.New("config not initialised") + } + + // just check to make sure that registry is initialised + _, ok = ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("service registry not initialised") + } + + srv := apiServer{config: cfg} + ctx[bootstrap.BootstrappedAPIServer] = srv + return nil +} diff --git a/api/bootstrapper_test.go b/api/bootstrapper_test.go new file mode 100644 index 000000000..0c62a19c8 --- /dev/null +++ b/api/bootstrapper_test.go @@ -0,0 +1,31 @@ +// +build unit + +package api + +import ( + "testing" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/node" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/stretchr/testify/assert" +) + +func TestBootstrapper_Bootstrap(t *testing.T) { + b := Bootstrapper{} + + // no config + m := make(map[string]interface{}) + err := b.Bootstrap(m) + assert.Error(t, err) + + // config + m[bootstrap.BootstrappedConfig] = new(testingconfig.MockConfig) + m[documents.BootstrappedRegistry] = documents.NewServiceRegistry() + err = b.Bootstrap(m) + assert.Nil(t, err) + assert.NotNil(t, m[bootstrap.BootstrappedAPIServer]) + _, ok := m[bootstrap.BootstrappedAPIServer].(node.Server) + assert.True(t, ok) +} diff --git a/api/insecure.go b/api/insecure.go index 342986937..e0f77cfc0 100644 --- a/api/insecure.go +++ b/api/insecure.go @@ -6,7 +6,7 @@ package api // Make sure you enter "localhost:8082" as the Common Name. const ( - InsecureKey = `-----BEGIN RSA PRIVATE KEY----- + insecureKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAyEnDbL/RxZrgDN85W958GvCnWYfLIl/yf3OnzRpSlhz5oKg6 hnigQeUiFlU60p8vnTbjr4idoyobYCTvqRh6oZk8r1zJ4H2Kx3HIIAkSgu1jzPV/ QwwbA0O4cJ9RttS3vFf9bDoJ4T93t8JNuRgF0jrQX8zPn/22/g1BrrhIV5gp8pWD @@ -33,7 +33,7 @@ FsU0FQKBgDS5XqHLIuN8isp2uOYzOLUqafStV/qAzvx9Tv6PNgBWLFekz9xpfxux yaQJtUNTfTXQ6tMjUpAwJam/G1h7ZTNYj2iuDVNlAgpwP45SkxWQ2dJEwtAyooVe kkVOdXE61p7fxhigyBb77uoX3adz4ECr3ktbAL2a0Z1XG9oTa2LW -----END RSA PRIVATE KEY-----` - InsecureCert = `-----BEGIN CERTIFICATE----- + insecureCert = `-----BEGIN CERTIFICATE----- MIIDmDCCAoACCQDHr6ZuK9By7zANBgkqhkiG9w0BAQsFADCBjTELMAkGA1UEBhMC VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x GDAWBgNVBAoMD0NlbnRyaWZ1Z2UgSW5jLjESMBAGA1UEAwwJbG9jYWxob3N0MSMw diff --git a/api/server.go b/api/server.go index 656b5159e..b0e77cc65 100644 --- a/api/server.go +++ b/api/server.go @@ -1,19 +1,17 @@ package api -// LICENSE: Apache -// This is taken from https://github.com/philips/grpc-gateway-example/ -// PLEASE DO NOT call any config.* stuff here as it creates dependencies that can't be injected easily when testing - import ( "crypto/tls" "crypto/x509" - "errors" "net" "net/http" + _ "net/http/pprof" // we need this side effect that loads the pprof endpoints to defaultServerMux "strings" "sync" "time" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/centerrors" "github.com/grpc-ecosystem/grpc-gateway/runtime" logging "github.com/ipfs/go-log" @@ -22,33 +20,27 @@ import ( "google.golang.org/grpc/credentials" ) -var log = logging.Logger("cent-api-server") +var log = logging.Logger("api-server") -// CentAPIServer is an implementation of node.Server interface for serving HTTP based Centrifuge API -type CentAPIServer struct { - Address string - Port int - CentNetwork string +// Config defines methods required for the package api +type Config interface { + GetServerAddress() string + GetServerPort() int + GetNetworkString() string + IsPProfEnabled() bool } -func NewCentAPIServer( - address string, - port int, - centNetwork string, -) *CentAPIServer { - return &CentAPIServer{ - Address: address, - Port: port, - CentNetwork: centNetwork, - } +// apiServer is an implementation of node.Server interface for serving HTTP based Centrifuge API +type apiServer struct { + config Config } -func (*CentAPIServer) Name() string { - return "CentAPIServer" +func (apiServer) Name() string { + return "APIServer" } // Serve exposes the client APIs for interacting with a centrifuge node -func (c *CentAPIServer) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) { +func (c apiServer) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) { defer wg.Done() certPool, err := loadCertPool() if err != nil { @@ -58,8 +50,8 @@ func (c *CentAPIServer) Start(ctx context.Context, wg *sync.WaitGroup, startupEr if err != nil { startupErr <- err } - addr := c.Address + addr := c.config.GetServerAddress() creds := credentials.NewTLS(&tls.Config{ RootCAs: certPool, ServerName: addr, @@ -81,12 +73,17 @@ func (c *CentAPIServer) Start(ctx context.Context, wg *sync.WaitGroup, startupEr mux := http.NewServeMux() gwmux := runtime.NewServeMux() - err = registerServices(ctx, grpcServer, gwmux, addr, dopts) + err = registerServices(ctx, c.config, grpcServer, gwmux, addr, dopts) if err != nil { startupErr <- err return } + if c.config.IsPProfEnabled() { + log.Info("added pprof endpoints to the server") + mux.Handle("/debug/", http.DefaultServeMux) + } + mux.Handle("/", gwmux) srv := &http.Server{ Addr: addr, @@ -99,13 +96,14 @@ func (c *CentAPIServer) Start(ctx context.Context, wg *sync.WaitGroup, startupEr startUpErrOut := make(chan error) go func(startUpErrInner chan<- error) { - conn, err := net.Listen("tcp", c.Address) + conn, err := net.Listen("tcp", c.config.GetServerAddress()) if err != nil { startUpErrInner <- err return } - log.Infof("HTTP/gRpc listening on Port: %d\n", c.Port) - log.Infof("Connecting to Network: %s\n", c.CentNetwork) + + log.Infof("HTTP/gRpc listening on Port: %d\n", c.config.GetServerPort()) + log.Infof("Connecting to Network: %s\n", c.config.GetNetworkString()) err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)) if err != nil { startUpErrInner <- err @@ -113,32 +111,29 @@ func (c *CentAPIServer) Start(ctx context.Context, wg *sync.WaitGroup, startupEr }(startUpErrOut) // listen to context events as well as http server startup errors - for { - select { - case err := <-startUpErrOut: - // this could create an issue if the listeners are blocking. - // We need to only propagate the error if its an error other than a server closed - if err != nil && err.Error() != http.ErrServerClosed.Error() { - startupErr <- err - return - } - // most probably a graceful shutdown - log.Info(err) - return - case <-ctx.Done(): - ctxn, _ := context.WithTimeout(context.Background(), 1*time.Second) - // gracefully shutdown the server - // we can only do this because srv is thread safe - log.Info("Shutting down API server") - err := srv.Shutdown(ctxn) - if err != nil { - panic(err) - } - log.Info("API server stopped") + select { + case err := <-startUpErrOut: + // this could create an issue if the listeners are blocking. + // We need to only propagate the error if its an error other than a server closed + if err != nil && err.Error() != http.ErrServerClosed.Error() { + startupErr <- err return } + // most probably a graceful shutdown + log.Info(err) + return + case <-ctx.Done(): + ctxn, _ := context.WithTimeout(context.Background(), 1*time.Second) + // gracefully shutdown the server + // we can only do this because srv is thread safe + log.Info("Shutting down API server") + err := srv.Shutdown(ctxn) + if err != nil { + panic(err) + } + log.Info("API server stopped") + return } - } // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC @@ -156,7 +151,7 @@ func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Ha func loadCertPool() (certPool *x509.CertPool, err error) { certPool = x509.NewCertPool() - ok := certPool.AppendCertsFromPEM([]byte(InsecureCert)) + ok := certPool.AppendCertsFromPEM([]byte(insecureCert)) if !ok { return nil, centerrors.Wrap(errors.New("could not load certpool"), "") } @@ -164,7 +159,7 @@ func loadCertPool() (certPool *x509.CertPool, err error) { } func loadKeyPair() (keyPair tls.Certificate, err error) { - pair, err := tls.X509KeyPair([]byte(InsecureCert), []byte(InsecureKey)) + pair, err := tls.X509KeyPair([]byte(insecureCert), []byte(insecureKey)) if err != nil { return pair, err } diff --git a/api/server_test.go b/api/server_test.go index 7ff2f6f99..ac03a101e 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -9,26 +9,52 @@ import ( "sync" "testing" + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/centrifuge-protobufs/documenttypes" + "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/context/testlogging" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" - "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/nft" + "github.com/centrifuge/go-centrifuge/p2p" + "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration +var registry *documents.ServiceRegistry + func TestMain(m *testing.M) { + ethClient := &testingcommons.MockEthClient{} + ethClient.On("GetEthClient").Return(nil) + ctx[ethereum.BootstrappedEthereumClient] = ethClient + ibootstappers := []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, &storage.Bootstrapper{}, + &queue.Bootstrapper{}, + anchors.Bootstrapper{}, + &identity.Bootstrapper{}, + documents.Bootstrapper{}, + p2p.Bootstrapper{}, &invoice.Bootstrapper{}, &purchaseorder.Bootstrapper{}, + &nft.Bootstrapper{}, + &queue.Starter{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + registry = ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) flag.Parse() result := m.Run() bootstrap.RunTestTeardown(ibootstappers) @@ -36,9 +62,12 @@ func TestMain(m *testing.M) { } func TestCentAPIServer_StartContextCancel(t *testing.T) { - documents.GetRegistryInstance().Register(documenttypes.InvoiceDataTypeUrl, invoice.DefaultService(nil, nil, nil)) - capi := NewCentAPIServer("0.0.0.0:9000", 9000, "") - ctx, canc := context.WithCancel(context.Background()) + cfg.Set("nodeHostname", "0.0.0.0") + cfg.Set("nodePort", 9000) + cfg.Set("centrifugeNetwork", "") + registry.Register(documenttypes.InvoiceDataTypeUrl, invoice.DefaultService(cfg, nil, nil, nil, nil)) + capi := apiServer{config: cfg} + ctx, canc := context.WithCancel(context.WithValue(context.Background(), bootstrap.NodeObjRegistry, ctx)) startErr := make(chan error) var wg sync.WaitGroup wg.Add(1) @@ -50,8 +79,11 @@ func TestCentAPIServer_StartContextCancel(t *testing.T) { func TestCentAPIServer_StartListenError(t *testing.T) { // cause an error by using an invalid port - capi := NewCentAPIServer("0.0.0.0:100000000", 100000000, "") - ctx, _ := context.WithCancel(context.Background()) + cfg.Set("nodeHostname", "0.0.0.0") + cfg.Set("nodePort", 100000000) + cfg.Set("centrifugeNetwork", "") + ctx, _ := context.WithCancel(context.WithValue(context.Background(), bootstrap.NodeObjRegistry, ctx)) + capi := apiServer{config: cfg} startErr := make(chan error) var wg sync.WaitGroup wg.Add(1) @@ -61,3 +93,20 @@ func TestCentAPIServer_StartListenError(t *testing.T) { assert.NotNil(t, err, "Error should be not nil") assert.Equal(t, "listen tcp: address 100000000: invalid port", err.Error()) } + +func TestCentAPIServer_FailedToGetRegistry(t *testing.T) { + // cause an error by using an invalid port + cfg.Set("nodeHostname", "0.0.0.0") + cfg.Set("nodePort", 100000000) + cfg.Set("centrifugeNetwork", "") + ctx, _ := context.WithCancel(context.Background()) + capi := apiServer{config: cfg} + startErr := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + go capi.Start(ctx, &wg, startErr) + err := <-startErr + wg.Wait() + assert.NotNil(t, err, "Error should be not nil") + assert.Equal(t, "failed to get NodeObjRegistry", err.Error()) +} diff --git a/api/service.go b/api/service.go index c67ffd193..70a9e1135 100644 --- a/api/service.go +++ b/api/service.go @@ -1,11 +1,12 @@ package api import ( - "fmt" - + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/healthcheck" "github.com/centrifuge/go-centrifuge/nft" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/documents" @@ -19,16 +20,33 @@ import ( ) // registerServices registers all endpoints to the grpc server -func registerServices(ctx context.Context, grpcServer *grpc.Server, gwmux *runtime.ServeMux, addr string, dopts []grpc.DialOption) error { +func registerServices(ctx context.Context, cfg Config, grpcServer *grpc.Server, gwmux *runtime.ServeMux, addr string, dopts []grpc.DialOption) error { + // node object registry + nodeObjReg, ok := ctx.Value(bootstrap.NodeObjRegistry).(map[string]interface{}) + if !ok { + return errors.New("failed to get %s", bootstrap.NodeObjRegistry) + } + + // load dependencies + registry, ok := nodeObjReg[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("failed to get %s", documents.BootstrappedRegistry) + } + payObService, ok := nodeObjReg[nft.BootstrappedPayObService].(nft.PaymentObligation) + if !ok { + return errors.New("failed to get %s", nft.BootstrappedPayObService) + } + // documents (common) - documentpb.RegisterDocumentServiceServer(grpcServer, documents.GRPCHandler()) + documentpb.RegisterDocumentServiceServer(grpcServer, documents.GRPCHandler(registry)) err := documentpb.RegisterDocumentServiceHandlerFromEndpoint(ctx, gwmux, addr, dopts) if err != nil { return err } // invoice - handler, err := invoice.GRPCHandler() + invCfg := cfg.(config.Configuration) + handler, err := invoice.GRPCHandler(invCfg, registry) if err != nil { return err } @@ -39,9 +57,10 @@ func registerServices(ctx context.Context, grpcServer *grpc.Server, gwmux *runti } // purchase orders - srv, err := purchaseorder.GRPCHandler() + poCfg := cfg.(config.Configuration) + srv, err := purchaseorder.GRPCHandler(poCfg, registry) if err != nil { - return fmt.Errorf("failed to get purchase order handler: %v", err) + return errors.New("failed to get purchase order handler: %v", err) } purchaseorderpb.RegisterDocumentServiceServer(grpcServer, srv) @@ -51,13 +70,14 @@ func registerServices(ctx context.Context, grpcServer *grpc.Server, gwmux *runti } // healthcheck - healthpb.RegisterHealthCheckServiceServer(grpcServer, healthcheck.GRPCHandler()) + hcCfg := cfg.(healthcheck.Config) + healthpb.RegisterHealthCheckServiceServer(grpcServer, healthcheck.GRPCHandler(hcCfg)) err = healthpb.RegisterHealthCheckServiceHandlerFromEndpoint(ctx, gwmux, addr, dopts) if err != nil { return err } - nftpb.RegisterNFTServiceServer(grpcServer, nft.GRPCHandler()) + nftpb.RegisterNFTServiceServer(grpcServer, nft.GRPCHandler(payObService)) err = nftpb.RegisterNFTServiceHandlerFromEndpoint(ctx, gwmux, addr, dopts) if err != nil { return err diff --git a/api/test_bootstrapper.go b/api/test_bootstrapper.go new file mode 100644 index 000000000..99f2e4a36 --- /dev/null +++ b/api/test_bootstrapper.go @@ -0,0 +1,11 @@ +// +build unit integration + +package api + +func (b Bootstrapper) TestBootstrap(ctx map[string]interface{}) error { + return b.Bootstrap(ctx) +} + +func (b Bootstrapper) TestTearDown() error { + return nil +} diff --git a/bootstrap/bootstrapper.go b/bootstrap/bootstrapper.go index b371a32a3..5bb8bac1b 100644 --- a/bootstrap/bootstrapper.go +++ b/bootstrap/bootstrapper.go @@ -2,11 +2,13 @@ package bootstrap // DO NOT PUT any app logic in this package to avoid any dependency cycles +// Bootstrap constants are keys to mapped value in bootstrapped context const ( - BootstrappedConfig string = "BootstrappedConfig" - BootstrappedLevelDb string = "BootstrappedLevelDb" - BootstrappedEthereumClient string = "BootstrappedEthereumClient" - BootstrappedAnchorRepository string = "BootstrappedAnchorRepository" + BootstrappedConfig string = "BootstrappedConfig" + BootstrappedP2PServer string = "BootstrappedP2PServer" + BootstrappedAPIServer string = "BootstrappedAPIServer" + BootstrappedQueueServer string = "BootstrappedQueueServer" + NodeObjRegistry string = "NodeObjRegistry" ) // Bootstrapper must be implemented by all packages that needs bootstrapping at application start diff --git a/bootstrap/test_bootstrapper.go b/bootstrap/test_bootstrapper.go index 2d01f03aa..65d52646e 100644 --- a/bootstrap/test_bootstrapper.go +++ b/bootstrap/test_bootstrapper.go @@ -12,12 +12,12 @@ type TestBootstrapper interface { TestTearDown() error } -func RunTestBootstrappers(bootstrappers []TestBootstrapper, context map[string]interface{}) { - if context == nil { - context = map[string]interface{}{} +func RunTestBootstrappers(bootstrappers []TestBootstrapper, ctx map[string]interface{}) { + if ctx == nil { + ctx = map[string]interface{}{} } for _, b := range bootstrappers { - err := b.TestBootstrap(context) + err := b.TestBootstrap(ctx) if err != nil { panic(err) } diff --git a/build/configs/default_config.yaml b/build/configs/default_config.yaml index 85518be7d..5694a9a76 100644 --- a/build/configs/default_config.yaml +++ b/build/configs/default_config.yaml @@ -7,8 +7,8 @@ networks: id: 333 ethereumNetworkId: 8383 bootstrapPeers: - - "/ip4/127.0.0.1/tcp/38202/ipfs/QmTQxbwkuZYYDfuzTbxEAReTNCLozyy558vQngVvPMjLYk" - - "/ip4/127.0.0.1/tcp/38203/ipfs/QmVf6EN6mkqWejWKW2qPu16XpdG3kJo1T3mhahPB5Se5n1" + - "/ip4/127.0.0.1/tcp/38202/ipfs/QmTQxbwkuZYYDfuzTbxEAReTNCLozyy558vQngVvPMjLYk" + - "/ip4/127.0.0.1/tcp/38203/ipfs/QmVf6EN6mkqWejWKW2qPu16XpdG3kJo1T3mhahPB5Se5n1" contractAddresses: identityFactory: "" identityRegistry: "" @@ -21,8 +21,8 @@ networks: id: 51 # Bootstrap list of nodes that Centrifuge provides to the russianhill testnet bootstrapPeers: - - "/ip4/35.225.200.42/tcp/38202/ipfs/12D3KooWLiicQVwThTBY6xKcPoLf6RQYJFpwf1r75wLx2ZR3pCd1" - - "/ip4/35.225.86.210/tcp/38202/ipfs/12D3KooWQZMA8GPHrvEZB9wdkoUcAAmCZHp9eyyZ4SE8gFr3hTNX" + - "/ip4/35.225.200.42/tcp/38202/ipfs/12D3KooWLiicQVwThTBY6xKcPoLf6RQYJFpwf1r75wLx2ZR3pCd1" + - "/ip4/35.225.86.210/tcp/38202/ipfs/12D3KooWQZMA8GPHrvEZB9wdkoUcAAmCZHp9eyyZ4SE8gFr3hTNX" # Ethereum network ID - Rinkeby ethereumNetworkId: 4 # Latest deployed Smart Contracts for the given testnet @@ -30,7 +30,54 @@ networks: identityFactory: "0x90d294571e73842697a66b7a99a09dd6c73d356d" identityRegistry: "0x9660c039d311453af0d58c5666723d9c2fa7d6ec" anchorRepository: "0x7f854dfa98012d7fa55c803bba2260bcdee4b5ed" - paymentObligation: "" + paymentObligation: "0xdb0581A9328664855328AdDb0E251184640f9e5D" + + bernalheights: + ### + # Kovan FAQ + # - With infura you get an error - "This request is not supported because your node is running with state pruning. Run with --pruning=archive.", + # what to do? Run a local parity node with kovan eg: with `parity --chain=kovan --port=30304 --warp --warp-barrier 5680000 --no-ancient-blocks --no-serve-light --max-peers 250 --snapshot-peers 50 --min-peers 50 --mode active --tracing off --pruning=archive --db-compaction ssd --cache-size 4096 --jsonrpc-hosts all --jsonrpc-interface all` + # - With local parity node you get an error - "Blocked connection to WebSockets server from untrusted origin: .." + # what to do? Run the parity node with `--unsafe-expose` flag + ### + # Numeric ID of the Centrifuge network + id: 52 + # Bootstrap list of nodes that Centrifuge provides to the bernalheights testnet + bootstrapPeers: + - "/ip4/104.154.18.51/tcp/38202/ipfs/12D3KooWPs6iaeUuFZNu1GxvsyBTSrTs9vtB6btMAnHFoLjbkzCa" + - "/ip4/104.155.185.237/tcp/38202/ipfs/12D3KooWPCGcwiTjoKWHfBa482UPtaeUxNLwd8zbnB1S7weAZUxZ" + # Ethereum network ID - Kovan + ethereumNetworkId: 42 + # Latest deployed Smart Contracts for the given testnet + contractAddresses: + identityFactory: "0x85b32f7a3f40481f12334041670c8cbe07f7d79c" + identityRegistry: "0x54ae373f096faf2db6b8a46717c0b98ecfa075dd" + anchorRepository: "0x444f649e307442e76ccf737466e52f1609b98260" + paymentObligation: "0x0417eb37941164368401D666984cED7694ABcBb1" + +# Data Storage +storage: + # Path for levelDB file + path: /tmp/centrifuge_data.leveldb + +# Configuration Storage +configStorage: + # Path for levelDB file + path: /tmp/centrifuge_config_data.leveldb + +# Interface where the API and P2P Server listens to +nodeHostname: 0.0.0.0 +# Port where API Server listens to +nodePort: 8082 + +# Peer-to-peer configurations +p2p: + # Specify External IP where the node can be reached at if behind NAT + #externalIP: w.x.y.z + # Port used for the P2P layer + port: 38202 + # Timeout when opening connections to peers + connectTimeout: "30s" # Peer-to-peer configurations p2p: @@ -49,7 +96,7 @@ ethereum: # Selects which ethereum account to use of the ones provided in the custom config file defaultAccountName: "main" # Location of the ethereum client node (we require ws or ipc to be able to catch events) - nodeURL: ws://localhost:9546 + nodeURL: http://localhost:9545 # Default gas price gasPrice: 1000000000 # Default gas limit @@ -65,3 +112,7 @@ ethereum: # Disable when some ethereum clients do not support txpool api txPoolAccessEnabled: true +# any debugging config will go here +debug: + # pprof for debugging + pprof: false diff --git a/build/package-lock.json b/build/package-lock.json index 4eae7c3b5..58f0d65e4 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1,6 +1,6 @@ { "name": "go-centrifuge", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/build/package.json b/build/package.json index 8f6caa7fa..cd221df70 100644 --- a/build/package.json +++ b/build/package.json @@ -1,6 +1,6 @@ { "name": "go-centrifuge", - "version": "0.0.1", + "version": "0.0.2", "description": "Protobuf files & go bindings for go-centrifuge", "main": "index.js", "scripts": { diff --git a/build/resources/centrifuge_example.yaml b/build/resources/centrifuge_example.yaml index d41454d53..3abe8c7af 100644 --- a/build/resources/centrifuge_example.yaml +++ b/build/resources/centrifuge_example.yaml @@ -23,6 +23,10 @@ p2p: #externalIP: 100.111.112.113 port: 38202 +#Configure your node to push notifications to your custom WebHook +#notifications: +# endpoint: "https://webhook.site/9fa0b32f-c745-4367-acd6-a7ed418ef608" + ethereum: accounts: main: diff --git a/build/scripts/docker/entrypoint.sh b/build/scripts/docker/entrypoint.sh index 5cca2e683..f5834c4bc 100755 --- a/build/scripts/docker/entrypoint.sh +++ b/build/scripts/docker/entrypoint.sh @@ -4,4 +4,4 @@ set -x CENT_MODE=${CENT_MODE:-run} -/root/go-centrifuge ${CENT_MODE} --config /root/.centrifuge/config/config.yaml $@ +/root/centrifuge ${CENT_MODE} --config /root/.centrifuge/config/config.yaml $@ diff --git a/build/scripts/migrate.sh b/build/scripts/migrate.sh new file mode 100755 index 000000000..d93abeb28 --- /dev/null +++ b/build/scripts/migrate.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Allow passing parent directory as a parameter +PARENT_DIR=$1 +if [ -z ${PARENT_DIR} ]; +then + PARENT_DIR=`pwd` + echo "PARENT DIR ${PARENT_DIR}" +fi + +# Even if other `env_vars.sh` might hold this variable +# Let's not count on it and be clear instead +if [ -z ${CENT_ETHEREUM_CONTRACTS_DIR} ]; then + CENT_ETHEREUM_CONTRACTS_DIR=${PARENT_DIR}/vendor/github.com/centrifuge/centrifuge-ethereum-contracts +fi + +# Assure that all the dependencies for the contracts are installed +npm install --pwd ${CENT_ETHEREUM_CONTRACTS_DIR} --prefix=${CENT_ETHEREUM_CONTRACTS_DIR} + +# `truffle migrate` will fail if not executed in the sub-dir +cd ${CENT_ETHEREUM_CONTRACTS_DIR} +# Clear up previous build +rm -Rf ./build + + +LOCAL_ETH_CONTRACT_ADDRESSES="${CENT_ETHEREUM_CONTRACTS_DIR}/deployments/local.json" +if [ ! -e $LOCAL_ETH_CONTRACT_ADDRESSES ]; then + echo "$LOCAL_ETH_CONTRACT_ADDRESSES doesn't exist. Probably no migrations run yet. Forcing migrations." + FORCE_MIGRATE='true' +fi + +if [[ "X${FORCE_MIGRATE}" == "Xtrue" ]]; +then + echo "Running the Solidity contracts migrations for local geth" + ${CENT_ETHEREUM_CONTRACTS_DIR}/scripts/migrate.sh localgeth +else + echo "Not migrating the Solidity contracts" +fi + +cd ${PARENT_DIR} \ No newline at end of file diff --git a/build/scripts/test-dependencies/test-ethereum/env_vars.sh b/build/scripts/test-dependencies/test-ethereum/env_vars.sh index cdbc769d5..b38a38efe 100755 --- a/build/scripts/test-dependencies/test-ethereum/env_vars.sh +++ b/build/scripts/test-dependencies/test-ethereum/env_vars.sh @@ -7,7 +7,7 @@ IDENTITY=CentTestEth GETH_DOCKER_CONTAINER_NAME="geth-node" CENT_ETHEREUM_CONTRACTS_DIR=${PARENT_DIR}/vendor/github.com/centrifuge/centrifuge-ethereum-contracts CENT_ETHEREUM_CONTEXTWAITTIMEOUT="180s" -CENT_ETHEREUM_NODEURL=${CENT_ETHEREUM_NODEURL:-ws://localhost:$WS_PORT} +CENT_ETHEREUM_NODEURL=${CENT_ETHEREUM_NODEURL:-http://localhost:$RPC_PORT} CENT_ETHEREUM_GASLIMIT=4712388 CENT_ETHEREUM_GASPRICE=1000000000 CENT_ETHEREUM_GETH_START_TIMEOUT=${CENT_ETHEREUM_GETH_START_TIMEOUT_OVERRIDE:-600} # In Seconds, default 10 minutes diff --git a/build/scripts/test_wrapper.sh b/build/scripts/test_wrapper.sh index 23647e8ba..03b27d3d2 100755 --- a/build/scripts/test_wrapper.sh +++ b/build/scripts/test_wrapper.sh @@ -29,38 +29,9 @@ done ############################################################ ################# Prepare for tests ######################## -# Even if other `env_vars.sh` might hold this variable -# Let's not count on it and be clear instead -if [ -z ${CENT_ETHEREUM_CONTRACTS_DIR} ]; then - CENT_ETHEREUM_CONTRACTS_DIR=${PARENT_DIR}/vendor/github.com/centrifuge/centrifuge-ethereum-contracts -fi - -# Assure that all the dependencies for the contracts are installed -npm install --pwd ${CENT_ETHEREUM_CONTRACTS_DIR} --prefix=${CENT_ETHEREUM_CONTRACTS_DIR} - -# `truffle migrate` will fail if not executed in the sub-dir -cd ${CENT_ETHEREUM_CONTRACTS_DIR} -# Clear up previous build -rm -Rf ./build - - -LOCAL_ETH_CONTRACT_ADDRESSES="${CENT_ETHEREUM_CONTRACTS_DIR}/deployments/local.json" -if [ ! -e $LOCAL_ETH_CONTRACT_ADDRESSES ]; then - echo "$LOCAL_ETH_CONTRACT_ADDRESSES doesn't exist. Probably no migrations run yet. Forcing migrations." - FORCE_MIGRATE='true' -fi - -if [[ "X${FORCE_MIGRATE}" == "Xtrue" ]]; -then - echo "Running the Solidity contracts migrations for local geth" - ${CENT_ETHEREUM_CONTRACTS_DIR}/scripts/migrate.sh localgeth -else - echo "Not migrating the Solidity contracts" -fi +${PARENT_DIR}/build/scripts/migrate.sh status=$? -cd ${PARENT_DIR} - ############################################################ ################# Run Tests ################################ diff --git a/build/scripts/tests/run_integration_tests.sh b/build/scripts/tests/run_integration_tests.sh index 767336b17..f15ee24b3 100755 --- a/build/scripts/tests/run_integration_tests.sh +++ b/build/scripts/tests/run_integration_tests.sh @@ -24,4 +24,4 @@ for d in $(go list -tags=integration ./... | grep -v vendor); do fi done -exit $status +exit $status \ No newline at end of file diff --git a/build/scripts/tests/run_testworld.sh b/build/scripts/tests/run_testworld.sh new file mode 100755 index 000000000..0f4a4f54a --- /dev/null +++ b/build/scripts/tests/run_testworld.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +echo "Running Testworld" + +status=$? + +output="go test -race -coverprofile=profile.out -covermode=atomic -tags=testworld github.com/centrifuge/go-centrifuge/testworld 2>&1" +eval "$output" | while IFS= read -r line; do printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$line"; done +if [ ${PIPESTATUS[0]} -ne 0 ]; then + status=1 +fi + +if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out +fi + + +exit $status diff --git a/build/swagger_config.js b/build/swagger_config.js index bce806f5f..3d4747053 100644 --- a/build/swagger_config.js +++ b/build/swagger_config.js @@ -1,6 +1,6 @@ module.exports = { info: { - version: "0.0.1", + version: "0.0.2", title: "Centrifuge OS Node API", description: "\n", contact: { diff --git a/centerrors/error.go b/centerrors/error.go index 8ccb07cc9..91c6d8a56 100644 --- a/centerrors/error.go +++ b/centerrors/error.go @@ -11,18 +11,23 @@ import ( const ( // RequiredField error when required field is empty + // Deprecated: in favour of error types in each package RequiredField = "Required field" // NilDocument error when document passed is Nil + // Deprecated: in favour of error types in each package NilDocument = "Nil document" // IdentifierReUsed error when same identifier is re-used + // Deprecated: in favour of error types in each package IdentifierReUsed = "Identifier re-used" // NilDocumentData error when document data is Nil + // Deprecated: in favour of error types in each package NilDocumentData = "Nil document data" // RequirePositiveNumber error when amount or any such is zero or negative + // Deprecated: in favour of error types in each package RequirePositiveNumber = "Require positive number" ) @@ -45,7 +50,7 @@ func New(code code.Code, message string) error { } // NewWithErrors constructs a new error with code, error message, and errors -func NewWithErrors(c code.Code, message string, errors map[string]string) error { +func NewWithErrors(c code.Code, message string, errs map[string]string) error { if c == code.Ok { return nil } @@ -53,7 +58,7 @@ func NewWithErrors(c code.Code, message string, errors map[string]string) error return &errpb{ Code: int32(c), Message: message, - Errors: errors, + Errors: errs, } } @@ -106,12 +111,14 @@ func (p2pErr *P2PError) Errors() map[string]string { } // NilError returns error with Type added to message +// Deprecated: in favour of functions in `github.com/centrifuge/go-centrifuge/errors` func NilError(param interface{}) error { return errors.Errorf("NIL %v provided", reflect.TypeOf(param)) } // Wrap appends msg to errpb.Message if it is of type *errpb // else appends the msg to error through fmt.Errorf +// Deprecated: this is intended for use within p2p or api handlers only, For services and internal errors use the Error type defined in `github.com/centrifuge/go-centrifuge/errors` func Wrap(err error, msg string) error { if err == nil { return fmt.Errorf(msg) diff --git a/centerrors/error_test.go b/centerrors/error_test.go index 87588c108..ce386f589 100644 --- a/centerrors/error_test.go +++ b/centerrors/error_test.go @@ -3,11 +3,11 @@ package centerrors import ( - "fmt" "reflect" "testing" "github.com/centrifuge/go-centrifuge/code" + "github.com/centrifuge/go-centrifuge/errors" "github.com/magiconair/properties/assert" ) @@ -62,7 +62,7 @@ func TestP2PError(t *testing.T) { func TestWrap(t *testing.T) { // simple error - err := fmt.Errorf("simple-error") + err := errors.New("simple-error") err = Wrap(err, "wrapped error") assert.Equal(t, err.Error(), "wrapped error: simple-error") diff --git a/cmd/centrifuge/create_config.go b/cmd/centrifuge/create_config.go new file mode 100644 index 000000000..1868c5041 --- /dev/null +++ b/cmd/centrifuge/create_config.go @@ -0,0 +1,69 @@ +package main + +import ( + "os" + + "github.com/centrifuge/go-centrifuge/cmd" + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" +) + +var targetDataDir string +var ethNodeURL string +var accountKeyPath string +var accountPassword string +var network string +var apiPort int64 +var p2pPort int64 +var bootstraps []string +var txPoolAccess bool + +func init() { + home, err := homedir.Dir() + if err != nil { + log.Error(err) + os.Exit(1) + } + + var createConfigCmd = &cobra.Command{ + Use: "createconfig", + Short: "Configures Node", + Long: ``, + Run: func(c *cobra.Command, args []string) { + err := cmd.CreateConfig(targetDataDir, + ethNodeURL, + accountKeyPath, + accountPassword, + network, + apiPort, + p2pPort, + bootstraps, + txPoolAccess, + "", + nil) + if err != nil { + log.Info(targetDataDir, + accountKeyPath, + accountPassword, + network, + ethNodeURL, + apiPort, + p2pPort, + bootstraps, + txPoolAccess) + log.Fatalf("error: %v", err) + } + }, + } + + createConfigCmd.Flags().StringVarP(&targetDataDir, "targetdir", "t", home+"/datadir", "Target Data Dir") + createConfigCmd.Flags().StringVarP(ðNodeURL, "ethnodeurl", "e", "http://127.0.0.1:9545", "URL of Ethereum Client Node") + createConfigCmd.Flags().StringVarP(&accountKeyPath, "accountkeypath", "z", home+"/datadir/main.key", "Path of Ethereum Account Key JSON file") + createConfigCmd.Flags().StringVarP(&accountPassword, "accountpwd", "k", "", "Ethereum Account Password") + createConfigCmd.Flags().Int64VarP(&apiPort, "apiPort", "a", 8082, "Api Port") + createConfigCmd.Flags().Int64VarP(&p2pPort, "p2pPort", "p", 38202, "Peer-to-Peer Port") + createConfigCmd.Flags().StringVarP(&network, "network", "n", "russianhill", "Default Network") + createConfigCmd.Flags().StringSliceVarP(&bootstraps, "bootstraps", "b", nil, "Bootstrap P2P Nodes") + createConfigCmd.Flags().BoolVarP(&txPoolAccess, "txpoolaccess", "x", true, "Transaction Pool access") + rootCmd.AddCommand(createConfigCmd) +} diff --git a/cmd/generate_signing_key.go b/cmd/centrifuge/generate_signing_key.go similarity index 99% rename from cmd/generate_signing_key.go rename to cmd/centrifuge/generate_signing_key.go index c89b48fad..5ed688490 100644 --- a/cmd/generate_signing_key.go +++ b/cmd/centrifuge/generate_signing_key.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "github.com/centrifuge/go-centrifuge/keytools" diff --git a/cmd/centrifuge/main.go b/cmd/centrifuge/main.go new file mode 100644 index 000000000..736ef3102 --- /dev/null +++ b/cmd/centrifuge/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + Execute() +} diff --git a/cmd/manage_identities.go b/cmd/centrifuge/manage_identities.go similarity index 52% rename from cmd/manage_identities.go rename to cmd/centrifuge/manage_identities.go index 117ab8fb8..b3836f721 100644 --- a/cmd/manage_identities.go +++ b/cmd/centrifuge/manage_identities.go @@ -1,48 +1,51 @@ -package cmd +package main import ( "io/ioutil" + "github.com/centrifuge/go-centrifuge/cmd" "github.com/centrifuge/go-centrifuge/identity" "github.com/spf13/cobra" ) -var centrifugeIdString string +var centIDString string var purpose string var createIdentityCmd = &cobra.Command{ Use: "createidentity", Short: "creates identity with signing key as p2p id against ethereum", Long: "", - Run: func(cmd *cobra.Command, args []string) { - //cmd requires a config file - readConfigFile() - baseBootstrap() - identityService := identity.EthereumIdentityService{} - var centrifugeId identity.CentID + Run: func(cm *cobra.Command, args []string) { + //cm requires a config file + cfgFile = ensureConfigFile() + ctx, canc, _ := cmd.CommandBootstrap(cfgFile) + var centID identity.CentID var err error - if centrifugeIdString == "" { - centrifugeId = identity.NewRandomCentID() + if centIDString == "" { + centID = identity.RandomCentID() } else { - centrifugeId, err = identity.CentIDFromString(centrifugeIdString) + centID, err = identity.CentIDFromString(centIDString) if err != nil { panic(err) } } - _, confirmations, err := identityService.CreateIdentity(centrifugeId) + + idService := ctx[identity.BootstrappedIDService].(identity.Service) + _, confirmations, err := idService.CreateIdentity(centID) if err != nil { panic(err) } watchIdentity := <-confirmations - log.Infof("Identity created [%s]", watchIdentity.Identity.GetCentrifugeID().String()) + log.Infof("Identity created [%s]", watchIdentity.Identity.CentID().String()) // We need a way to return the identity created so it can be read by an automated process as well // when id autogenerated - id := []byte("{\"id\": \"" + centrifugeId.String() + "\"}") + id := []byte("{\"id\": \"" + centID.String() + "\"}") err = ioutil.WriteFile("newidentity.json", id, 0644) if err != nil { panic(err) } - log.Infof("Identity created [%s]", watchIdentity.Identity.GetCentrifugeID()) + log.Infof("Identity created [%s]", watchIdentity.Identity.CentID()) + canc() }, } @@ -51,17 +54,15 @@ var addKeyCmd = &cobra.Command{ Use: "addkey", Short: "add a signing key as p2p id against ethereum", Long: "add a signing key as p2p id against ethereum", - Run: func(cmd *cobra.Command, args []string) { - //cmd requires a config file - readConfigFile() - - baseBootstrap() - + Run: func(cm *cobra.Command, args []string) { + //cm requires a config file + cfgFile = ensureConfigFile() + ctx, canc, _ := cmd.CommandBootstrap(cfgFile) var purposeInt int switch purpose { case "p2p": - purposeInt = identity.KeyPurposeP2p + purposeInt = identity.KeyPurposeP2P case "sign": purposeInt = identity.KeyPurposeSigning case "ethauth": @@ -70,18 +71,20 @@ var addKeyCmd = &cobra.Command{ panic("Option not supported") } - err := identity.AddKeyFromConfig(purposeInt) + idService := ctx[identity.BootstrappedIDService].(identity.Service) + err := idService.AddKeyFromConfig(purposeInt) if err != nil { panic(err) } + canc() return }, } func init() { - createIdentityCmd.Flags().StringVarP(¢rifugeIdString, "centrifugeid", "i", "", "Centrifuge ID") - addKeyCmd.Flags().StringVarP(¢rifugeIdString, "centrifugeid", "i", "", "Centrifuge ID") + createIdentityCmd.Flags().StringVarP(¢IDString, "centrifugeid", "i", "", "Centrifuge ID") + addKeyCmd.Flags().StringVarP(¢IDString, "centrifugeid", "i", "", "Centrifuge ID") addKeyCmd.Flags().StringVarP(&purpose, "purpose", "p", "", "Key Purpose [p2p|sign|ethauth]") rootCmd.AddCommand(createIdentityCmd) rootCmd.AddCommand(addKeyCmd) diff --git a/cmd/root.go b/cmd/centrifuge/root.go similarity index 74% rename from cmd/root.go rename to cmd/centrifuge/root.go index 8e7f8009a..80046f0e8 100644 --- a/cmd/root.go +++ b/cmd/centrifuge/root.go @@ -1,11 +1,9 @@ -package cmd +package main import ( "fmt" "os" - "github.com/centrifuge/go-centrifuge/config" - cc "github.com/centrifuge/go-centrifuge/context" "github.com/centrifuge/go-centrifuge/utils" logging "github.com/ipfs/go-log" "github.com/mitchellh/go-homedir" @@ -21,7 +19,6 @@ var verbose bool var rootCmd = &cobra.Command{ Use: "centrifuge", Short: "Centrifuge protocol node", - Long: `POC for centrifuge app`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -52,8 +49,8 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set loglevel to debug") } -// readConfigFile reads in config file and ENV variables if set. -func readConfigFile() { +// ensureConfigFile ensures a config file is provided +func ensureConfigFile() string { if cfgFile == "" { // Find home directory. home, err := homedir.Dir() @@ -68,8 +65,7 @@ func readConfigFile() { cfgFile = "" } } - // If a config file is found, read it in. - config.Bootstrap(cfgFile) + return cfgFile } //setCentrifugeLoggers sets the loggers. @@ -85,23 +81,3 @@ func setCentrifugeLoggers() { logging.SetAllLoggers(gologging.INFO) } - -func runBootstrap() { - mb := cc.MainBootstrapper{} - mb.PopulateRunBootstrappers() - err := mb.Bootstrap(map[string]interface{}{}) - if err != nil { - // application must not continue to run - panic(err) - } -} - -func baseBootstrap() { - mb := cc.MainBootstrapper{} - mb.PopulateBaseBootstrappers() - err := mb.Bootstrap(map[string]interface{}{}) - if err != nil { - // application must not continue to run - panic(err) - } -} diff --git a/cmd/run.go b/cmd/centrifuge/run.go similarity index 56% rename from cmd/run.go rename to cmd/centrifuge/run.go index 3163580e9..26c23f1e7 100644 --- a/cmd/run.go +++ b/cmd/centrifuge/run.go @@ -1,6 +1,7 @@ -package cmd +package main import ( + "github.com/centrifuge/go-centrifuge/cmd" "github.com/spf13/cobra" ) @@ -9,11 +10,11 @@ var runCmd = &cobra.Command{ Use: "run", Short: "run a centrifuge node", Long: ``, - Run: func(cmd *cobra.Command, args []string) { - //cmd requires a config file - readConfigFile() + Run: func(cm *cobra.Command, args []string) { + //cm requires a config file + cfgFile := ensureConfigFile() // the following call will block - runBootstrap() + cmd.RunBootstrap(cfgFile) }, } diff --git a/cmd/sign_message.go b/cmd/centrifuge/sign_message.go similarity index 99% rename from cmd/sign_message.go rename to cmd/centrifuge/sign_message.go index 449df1ad5..a69ef7892 100644 --- a/cmd/sign_message.go +++ b/cmd/centrifuge/sign_message.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "fmt" diff --git a/cmd/verify_message.go b/cmd/centrifuge/verify_message.go similarity index 99% rename from cmd/verify_message.go rename to cmd/centrifuge/verify_message.go index bb15325ee..5424a171b 100644 --- a/cmd/verify_message.go +++ b/cmd/centrifuge/verify_message.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "fmt" diff --git a/cmd/version.go b/cmd/centrifuge/version.go similarity index 96% rename from cmd/version.go rename to cmd/centrifuge/version.go index 1b3df46ad..dd2938bf7 100644 --- a/cmd/version.go +++ b/cmd/centrifuge/version.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "fmt" diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 000000000..ca7d7314f --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,151 @@ +package cmd + +import ( + "context" + + "github.com/centrifuge/go-centrifuge/storage" + + logging "github.com/ipfs/go-log" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + c "github.com/centrifuge/go-centrifuge/context" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/keytools" + "github.com/centrifuge/go-centrifuge/node" + "github.com/centrifuge/go-centrifuge/queue" + "github.com/syndtr/goleveldb/leveldb" +) + +var log = logging.Logger("centrifuge-cmd") + +func createIdentity(idService identity.Service) (identity.CentID, error) { + centID := identity.RandomCentID() + _, confirmations, err := idService.CreateIdentity(centID) + if err != nil { + return [identity.CentIDLength]byte{}, err + } + _ = <-confirmations + + return centID, nil +} + +func generateKeys(config config.Configuration) { + p2pPub, p2pPvt := config.GetSigningKeyPair() + ethAuthPub, ethAuthPvt := config.GetEthAuthKeyPair() + keytools.GenerateSigningKeyPair(p2pPub, p2pPvt, "ed25519") + keytools.GenerateSigningKeyPair(p2pPub, p2pPvt, "ed25519") + keytools.GenerateSigningKeyPair(ethAuthPub, ethAuthPvt, "secp256k1") +} + +func addKeys(idService identity.Service) error { + err := idService.AddKeyFromConfig(identity.KeyPurposeP2P) + if err != nil { + panic(err) + } + err = idService.AddKeyFromConfig(identity.KeyPurposeSigning) + if err != nil { + panic(err) + } + err = idService.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) + if err != nil { + panic(err) + } + return nil +} + +// CreateConfig creates a config file using provide parameters and the default config +func CreateConfig( + targetDataDir, ethNodeURL, accountKeyPath, accountPassword, network string, + apiPort, p2pPort int64, + bootstraps []string, + txPoolAccess bool, + p2pConnectionTimeout string, + smartContractAddrs *config.SmartContractAddresses) error { + + data := map[string]interface{}{ + "targetDataDir": targetDataDir, + "accountKeyPath": accountKeyPath, + "accountPassword": accountPassword, + "network": network, + "ethNodeURL": ethNodeURL, + "bootstraps": bootstraps, + "apiPort": apiPort, + "p2pPort": p2pPort, + "p2pConnectTimeout": p2pConnectionTimeout, + "txpoolaccess": txPoolAccess, + } + if smartContractAddrs != nil { + data["smartContractAddresses"] = smartContractAddrs + } + v, err := config.CreateConfigFile(data) + if err != nil { + return err + } + log.Infof("Config File Created: %s\n", v.ConfigFileUsed()) + ctx, canc, _ := CommandBootstrap(v.ConfigFileUsed()) + cfg := ctx[bootstrap.BootstrappedConfig].(config.Configuration) + generateKeys(cfg) + + idService := ctx[identity.BootstrappedIDService].(identity.Service) + id, err := createIdentity(idService) + if err != nil { + return err + } + v.Set("identityId", id.String()) + err = v.WriteConfig() + if err != nil { + return err + } + cfg.Set("identityId", id.String()) + log.Infof("Identity created [%s] [%x]", id.String(), id) + err = addKeys(idService) + if err != nil { + return err + } + canc() + db := ctx[storage.BootstrappedLevelDB].(*leveldb.DB) + dbCfg := ctx[storage.BootstrappedConfigLevelDB].(*leveldb.DB) + db.Close() + dbCfg.Close() + return nil +} + +// RunBootstrap bootstraps the node for running +func RunBootstrap(cfgFile string) { + mb := c.MainBootstrapper{} + mb.PopulateRunBootstrappers() + ctx := map[string]interface{}{} + ctx[config.BootstrappedConfigFile] = cfgFile + err := mb.Bootstrap(ctx) + if err != nil { + // application must not continue to run + panic(err) + } +} + +// BaseBootstrap bootstraps the node for testing purposes mainly +func BaseBootstrap(cfgFile string) map[string]interface{} { + mb := c.MainBootstrapper{} + mb.PopulateBaseBootstrappers() + ctx := map[string]interface{}{} + ctx[config.BootstrappedConfigFile] = cfgFile + err := mb.Bootstrap(ctx) + if err != nil { + // application must not continue to run + panic(err) + } + return ctx +} + +// CommandBootstrap bootstraps the node for one time commands +func CommandBootstrap(cfgFile string) (map[string]interface{}, context.CancelFunc, error) { + ctx := BaseBootstrap(cfgFile) + queueSrv := ctx[bootstrap.BootstrappedQueueServer].(*queue.Server) + // init node with only the queue server which is needed by commands + n := node.New([]node.Server{queueSrv}) + cx, canc := context.WithCancel(context.Background()) + e := make(chan error) + go n.Start(cx, e) + return ctx, canc, nil +} diff --git a/cmd/create_config.go b/cmd/create_config.go deleted file mode 100644 index 3120c1fa6..000000000 --- a/cmd/create_config.go +++ /dev/null @@ -1,120 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/keytools" - "github.com/mitchellh/go-homedir" - "github.com/spf13/cobra" -) - -var targetDataDir string -var ethNodeUrl string -var accountKeyPath string -var accountPassword string -var network string -var apiPort int64 -var p2pPort int64 -var bootstraps []string - -func createIdentity() (identity.CentID, error) { - centrifugeId := identity.NewRandomCentID() - _, confirmations, err := identity.IDService.CreateIdentity(centrifugeId) - if err != nil { - return [identity.CentIDLength]byte{}, err - } - _ = <-confirmations - - return centrifugeId, nil -} - -func generateKeys() { - p2pPub, p2pPvt := config.Config.GetSigningKeyPair() - ethAuthPub, ethAuthPvt := config.Config.GetEthAuthKeyPair() - keytools.GenerateSigningKeyPair(p2pPub, p2pPvt, "ed25519") - keytools.GenerateSigningKeyPair(p2pPub, p2pPvt, "ed25519") - keytools.GenerateSigningKeyPair(ethAuthPub, ethAuthPvt, "secp256k1") -} - -func addKeys() error { - err := identity.AddKeyFromConfig(identity.KeyPurposeP2p) - if err != nil { - panic(err) - } - err = identity.AddKeyFromConfig(identity.KeyPurposeSigning) - if err != nil { - panic(err) - } - err = identity.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) - if err != nil { - panic(err) - } - return nil -} - -func init() { - home, err := homedir.Dir() - if err != nil { - log.Error(err) - os.Exit(1) - } - - var createConfigCmd = &cobra.Command{ - Use: "createconfig", - Short: "Configures Node", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - - data := map[string]interface{}{ - "targetDataDir": targetDataDir, - "accountKeyPath": accountKeyPath, - "accountPassword": accountPassword, - "network": network, - "ethNodeUrl": ethNodeUrl, - "bootstraps": bootstraps, - "apiPort": apiPort, - "p2pPort": p2pPort, - } - - v, err := config.CreateConfigFile(data) - if err != nil { - panic(err) - } - log.Infof("Config File Created: %s\n", v.ConfigFileUsed()) - - config.Bootstrap(v.ConfigFileUsed()) - generateKeys() - baseBootstrap() - id, err := createIdentity() - if err != nil { - panic(err) - } - - v.Set("identityId", id.String()) - err = v.WriteConfig() - if err != nil { - log.Fatalf("error: %v", err) - } - config.Config.V.Set("identityId", id.String()) - - log.Infof("Identity created [%s] [%x]", id.String(), id.ByteArray()) - - err = addKeys() - if err != nil { - log.Fatalf("error: %v", err) - } - }, - } - - createConfigCmd.Flags().StringVarP(&targetDataDir, "targetdir", "t", home+"/datadir", "Target Data Dir") - createConfigCmd.Flags().StringVarP(ðNodeUrl, "ethnodeurl", "e", "ws://127.0.0.1:9546", "URL of Ethereum Client Node (WS only)") - createConfigCmd.Flags().StringVarP(&accountKeyPath, "accountkeypath", "z", home+"/datadir/main.key", "Path of Ethereum Account Key JSON file") - createConfigCmd.Flags().StringVarP(&accountPassword, "accountpwd", "k", "", "Ethereum Account Password") - createConfigCmd.Flags().Int64VarP(&apiPort, "apiPort", "a", 8082, "Api Port") - createConfigCmd.Flags().Int64VarP(&p2pPort, "p2pPort", "p", 38202, "Peer-to-Peer Port") - createConfigCmd.Flags().StringVarP(&network, "network", "n", "russianhill", "Default Network") - createConfigCmd.Flags().StringSliceVarP(&bootstraps, "bootstraps", "b", nil, "Bootstrap P2P Nodes") - rootCmd.AddCommand(createConfigCmd) -} diff --git a/config/bootstrapper.go b/config/bootstrapper.go index a83160468..934556882 100644 --- a/config/bootstrapper.go +++ b/config/bootstrapper.go @@ -1,14 +1,22 @@ package config -import ( - "github.com/centrifuge/go-centrifuge/bootstrap" +import "github.com/centrifuge/go-centrifuge/bootstrap" + +// Bootstrap constants are keys to the value mappings in context bootstrap. +const ( + BootstrappedConfigFile string = "BootstrappedConfigFile" ) -type Bootstrapper struct { -} +// Bootstrapper implements bootstrap.Bootstrapper to initialise config package. +type Bootstrapper struct{} +// Bootstrap takes the passed in config file, loads the config and puts the config back into context. func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - Config.InitializeViper() - context[bootstrap.BootstrappedConfig] = Config + if _, ok := context[BootstrappedConfigFile]; !ok { + return ErrConfigFileBootstrapNotFound + } + cfgFile := context[BootstrappedConfigFile].(string) + context[bootstrap.BootstrappedConfig] = LoadConfiguration(cfgFile) + return nil } diff --git a/config/configuration.go b/config/configuration.go index 952312d23..87f22fa0c 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -7,231 +7,336 @@ package config import ( "bytes" - "errors" "fmt" "io/ioutil" "math/big" "os" "strings" + "sync" "time" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/resources" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" logging "github.com/ipfs/go-log" + "github.com/spf13/cast" "github.com/spf13/viper" ) var log = logging.Logger("config") -var Config *Configuration -type Configuration struct { +// Configuration defines the methods that a config type should implement. +type Configuration interface { + // generic methods + IsSet(key string) bool + Set(key string, value interface{}) + SetDefault(key string, value interface{}) + Get(key string) interface{} + GetString(key string) string + GetBool(key string) bool + GetInt(key string) int + GetDuration(key string) time.Duration + + GetStoragePath() string + GetConfigStoragePath() string + GetP2PPort() int + GetP2PExternalIP() string + GetP2PConnectionTimeout() time.Duration + GetServerPort() int + GetServerAddress() string + GetNumWorkers() int + GetWorkerWaitTimeMS() int + GetEthereumNodeURL() string + GetEthereumContextReadWaitTimeout() time.Duration + GetEthereumContextWaitTimeout() time.Duration + GetEthereumIntervalRetry() time.Duration + GetEthereumMaxRetries() int + GetEthereumGasPrice() *big.Int + GetEthereumGasLimit() uint64 + GetTxPoolAccessEnabled() bool + GetNetworkString() string + GetNetworkKey(k string) string + GetContractAddressString(address string) string + GetContractAddress(address string) common.Address + GetBootstrapPeers() []string + GetNetworkID() uint32 + + // CentID specific configs (eg: for multi tenancy) + GetEthereumAccount(accountName string) (account *AccountConfig, err error) + GetEthereumDefaultAccountName() string + GetReceiveEventNotificationEndpoint() string + GetIdentityID() ([]byte, error) + GetSigningKeyPair() (pub, priv string) + GetEthAuthKeyPair() (pub, priv string) + + // debug specific methods + IsPProfEnabled() bool +} + +// configuration holds the configuration details for the node. +type configuration struct { + mu sync.RWMutex configFile string - V *viper.Viper + v *viper.Viper } +// AccountConfig holds the account details. type AccountConfig struct { Address string Key string Password string } -// IdentityConfig holds ID, public and private key of a single entity -type IdentityConfig struct { - ID []byte - PublicKey []byte - PrivateKey []byte +// IsSet check if the key is set in the config. +func (c *configuration) IsSet(key string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.v.IsSet(key) +} + +// Set update the key and the value it holds in the configuration. +func (c *configuration) Set(key string, value interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + c.v.Set(key, value) } -// GetStoragePath returns the data storage backend -func (c *Configuration) GetStoragePath() string { - return c.V.GetString("storage.Path") +// SetDefault sets the default value for the given key. +func (c *configuration) SetDefault(key string, value interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + c.v.SetDefault(key, value) } -// GetP2PPort returns P2P Port -func (c *Configuration) GetP2PPort() int { - return c.V.GetInt("p2p.port") +// Get returns associated value for the key. +func (c *configuration) Get(key string) interface{} { + return c.get(key) } -// GetP2PExternalIP returns P2P External IP -func (c *Configuration) GetP2PExternalIP() string { - return c.V.GetString("p2p.externalIP") +// GetString returns value string associated with key. +func (c *configuration) GetString(key string) string { + return cast.ToString(c.get(key)) } -// GetP2PConnectionTimeout returns P2P Connect Timeout -func (c *Configuration) GetP2PConnectionTimeout() time.Duration { - return c.V.GetDuration("p2p.connectTimeout") +// GetInt returns value int associated with key. +func (c *configuration) GetInt(key string) int { + return cast.ToInt(c.get(key)) } -//////////////////////////////////////////////////////////////////////////////// -// Notifications -//////////////////////////////////////////////////////////////////////////////// -func (c *Configuration) GetReceiveEventNotificationEndpoint() string { - return c.V.GetString("notifications.endpoint") +// GetBool returns value bool associated with key. +func (c *configuration) GetBool(key string) bool { + return cast.ToBool(c.get(key)) } -//////////////////////////////////////////////////////////////////////////////// -// Server -//////////////////////////////////////////////////////////////////////////////// +// GetDuration returns value duration associated with key. +func (c *configuration) GetDuration(key string) time.Duration { + return cast.ToDuration(c.get(key)) +} + +func (c *configuration) get(key string) interface{} { + c.mu.RLock() + defer c.mu.RUnlock() + return c.v.Get(key) +} -func (c *Configuration) GetServerPort() int { - return c.V.GetInt("nodePort") +// GetStoragePath returns the data storage backend. +func (c *configuration) GetStoragePath() string { + return c.GetString("storage.path") } -func (c *Configuration) GetServerAddress() string { - return fmt.Sprintf("%s:%s", c.V.GetString("nodeHostname"), c.V.GetString("nodePort")) +// GetConfigStoragePath returns the config storage backend. +func (c *configuration) GetConfigStoragePath() string { + return c.GetString("configStorage.path") } -//////////////////////////////////////////////////////////////////////////////// -// Queuing -//////////////////////////////////////////////////////////////////////////////// +// GetP2PPort returns P2P Port. +func (c *configuration) GetP2PPort() int { + return c.GetInt("p2p.port") +} + +// GetP2PExternalIP returns P2P External IP. +func (c *configuration) GetP2PExternalIP() string { + return c.GetString("p2p.externalIP") +} + +// GetP2PConnectionTimeout returns P2P Connect Timeout. +func (c *configuration) GetP2PConnectionTimeout() time.Duration { + return c.GetDuration("p2p.connectTimeout") +} -func (c *Configuration) GetNumWorkers() int { - return c.V.GetInt("queue.numWorkers") +// GetReceiveEventNotificationEndpoint returns the webhook endpoint defined in the config. +func (c *configuration) GetReceiveEventNotificationEndpoint() string { + return c.GetString("notifications.endpoint") } -func (c *Configuration) GetWorkerWaitTimeMS() int { - return c.V.GetInt("queue.workerWaitTimeMS") +// GetServerPort returns the defined server port in the config. +func (c *configuration) GetServerPort() int { + return c.GetInt("nodePort") } -//////////////////////////////////////////////////////////////////////////////// -// Ethereum -//////////////////////////////////////////////////////////////////////////////// -func (c *Configuration) GetEthereumNodeURL() string { - return c.V.GetString("ethereum.nodeURL") +// GetServerAddress returns the defined server address of form host:port in the config. +func (c *configuration) GetServerAddress() string { + return fmt.Sprintf("%s:%s", c.GetString("nodeHostname"), c.GetString("nodePort")) } -func (c *Configuration) GetEthereumContextReadWaitTimeout() time.Duration { - return c.V.GetDuration("ethereum.contextReadWaitTimeout") +// GetNumWorkers returns number of queue workers defined in the config. +func (c *configuration) GetNumWorkers() int { + return c.GetInt("queue.numWorkers") } -func (c *Configuration) GetEthereumContextWaitTimeout() time.Duration { - return c.V.GetDuration("ethereum.contextWaitTimeout") +// GetWorkerWaitTimeMS returns the queue worker sleep time between cycles. +func (c *configuration) GetWorkerWaitTimeMS() int { + return c.GetInt("queue.workerWaitTimeMS") } -func (c *Configuration) GetEthereumIntervalRetry() time.Duration { - return c.V.GetDuration("ethereum.intervalRetry") +// GetEthereumNodeURL returns the URL of the Ethereum Node. +func (c *configuration) GetEthereumNodeURL() string { + return c.GetString("ethereum.nodeURL") } -func (c *Configuration) GetEthereumMaxRetries() int { - return c.V.GetInt("ethereum.maxRetries") +// GetEthereumContextReadWaitTimeout returns the read duration to pass for context.Deadline. +func (c *configuration) GetEthereumContextReadWaitTimeout() time.Duration { + return c.GetDuration("ethereum.contextReadWaitTimeout") } -func (c *Configuration) GetEthereumGasPrice() *big.Int { - return big.NewInt(c.V.GetInt64("ethereum.gasPrice")) +// GetEthereumContextWaitTimeout returns the commit duration to pass for context.Deadline. +func (c *configuration) GetEthereumContextWaitTimeout() time.Duration { + return c.GetDuration("ethereum.contextWaitTimeout") } -func (c *Configuration) GetEthereumGasLimit() uint64 { - return uint64(c.V.GetInt64("ethereum.gasLimit")) +// GetEthereumIntervalRetry returns duration to wait between retries. +func (c *configuration) GetEthereumIntervalRetry() time.Duration { + return c.GetDuration("ethereum.intervalRetry") } -func (c *Configuration) GetEthereumDefaultAccountName() string { - return c.V.GetString("ethereum.defaultAccountName") +// GetEthereumMaxRetries returns the max acceptable retries. +func (c *configuration) GetEthereumMaxRetries() int { + return c.GetInt("ethereum.maxRetries") } -func (c *Configuration) GetEthereumAccount(accountName string) (account *AccountConfig, err error) { +// GetEthereumGasPrice returns the gas price to use for a ethereum transaction. +func (c *configuration) GetEthereumGasPrice() *big.Int { + return big.NewInt(cast.ToInt64(c.get("ethereum.gasPrice"))) +} + +// GetEthereumGasLimit returns the gas limit to use for a ethereum transaction. +func (c *configuration) GetEthereumGasLimit() uint64 { + return cast.ToUint64(c.get("ethereum.gasLimit")) +} + +// GetEthereumDefaultAccountName returns the default account to use for the transaction. +func (c *configuration) GetEthereumDefaultAccountName() string { + return c.GetString("ethereum.defaultAccountName") +} + +// GetEthereumAccount returns the account details associated with the account name. +func (c *configuration) GetEthereumAccount(accountName string) (account *AccountConfig, err error) { k := fmt.Sprintf("ethereum.accounts.%s", accountName) - if !c.V.IsSet(k) { - return nil, fmt.Errorf("no account found with account name %s", accountName) + if !c.IsSet(k) { + return nil, errors.New("no account found with account name %s", accountName) } // Workaround for bug https://github.com/spf13/viper/issues/309 && https://github.com/spf13/viper/issues/513 account = &AccountConfig{ - Address: c.V.GetString(fmt.Sprintf("%s.address", k)), - Key: c.V.GetString(fmt.Sprintf("%s.key", k)), - Password: c.V.GetString(fmt.Sprintf("%s.password", k)), + Address: c.GetString(fmt.Sprintf("%s.address", k)), + Key: c.GetString(fmt.Sprintf("%s.key", k)), + Password: c.GetString(fmt.Sprintf("%s.password", k)), } return account, nil } -// Important flag for concurrency handling. Disable if Ethereum client doesn't support txpool API (INFURA) -func (c *Configuration) GetTxPoolAccessEnabled() bool { - return c.V.GetBool("ethereum.txPoolAccessEnabled") +// GetTxPoolAccessEnabled returns if the node can check the txpool for nonce increment. +// Note:Important flag for concurrency handling. Disable if Ethereum client doesn't support txpool API (INFURA). +func (c *configuration) GetTxPoolAccessEnabled() bool { + return c.GetBool("ethereum.txPoolAccessEnabled") } -//////////////////////////////////////////////////////////////////////////////// -// Network Configuration -//////////////////////////////////////////////////////////////////////////////// -func (c *Configuration) GetNetworkString() string { - return c.V.GetString("centrifugeNetwork") +// GetNetworkString returns defined network the node is connected to. +func (c *configuration) GetNetworkString() string { + return c.GetString("centrifugeNetwork") } -func (c *Configuration) GetNetworkKey(k string) string { +// GetNetworkKey returns the specific key(k) value defined in the default network. +func (c *configuration) GetNetworkKey(k string) string { return fmt.Sprintf("networks.%s.%s", c.GetNetworkString(), k) } // GetContractAddressString returns the deployed contract address for a given contract. -func (c *Configuration) GetContractAddressString(contract string) (address string) { - return c.V.GetString(c.GetNetworkKey(fmt.Sprintf("contractAddresses.%s", contract))) +func (c *configuration) GetContractAddressString(contract string) (address string) { + return c.GetString(c.GetNetworkKey(fmt.Sprintf("contractAddresses.%s", contract))) } // GetContractAddress returns the deployed contract address for a given contract. -func (c *Configuration) GetContractAddress(contract string) (address common.Address) { +func (c *configuration) GetContractAddress(contract string) (address common.Address) { return common.HexToAddress(c.GetContractAddressString(contract)) } // GetBootstrapPeers returns the list of configured bootstrap nodes for the given network. -func (c *Configuration) GetBootstrapPeers() []string { - return c.V.GetStringSlice(c.GetNetworkKey("bootstrapPeers")) +func (c *configuration) GetBootstrapPeers() []string { + return cast.ToStringSlice(c.get(c.GetNetworkKey("bootstrapPeers"))) } // GetNetworkID returns the numerical network id. -func (c *Configuration) GetNetworkID() uint32 { - return uint32(c.V.GetInt(c.GetNetworkKey("id"))) +func (c *configuration) GetNetworkID() uint32 { + return uint32(c.GetInt(c.GetNetworkKey("id"))) } -// GetIdentityID returns the self centID -func (c *Configuration) GetIdentityID() ([]byte, error) { - id, err := hexutil.Decode(c.V.GetString("identityId")) +// GetIdentityID returns the self centID in bytes. +func (c *configuration) GetIdentityID() ([]byte, error) { + id, err := hexutil.Decode(c.GetString("identityId")) if err != nil { return nil, centerrors.Wrap(err, "can't read identityId from config") } return id, err } -func (c *Configuration) GetSigningKeyPair() (pub, priv string) { - return c.V.GetString("keys.signing.publicKey"), c.V.GetString("keys.signing.privateKey") +// GetSigningKeyPair returns the signing key pair. +func (c *configuration) GetSigningKeyPair() (pub, priv string) { + return c.GetString("keys.signing.publicKey"), c.GetString("keys.signing.privateKey") } -func (c *Configuration) GetEthAuthKeyPair() (pub, priv string) { - return c.V.GetString("keys.ethauth.publicKey"), c.V.GetString("keys.ethauth.privateKey") +// GetEthAuthKeyPair returns ethereum key pair. +func (c *configuration) GetEthAuthKeyPair() (pub, priv string) { + return c.GetString("keys.ethauth.publicKey"), c.GetString("keys.ethauth.privateKey") } -// Configuration Implementation -func NewConfiguration(configFile string) *Configuration { - c := Configuration{configFile: configFile} - return &c +// IsPProfEnabled returns true if the pprof is enabled +func (c *configuration) IsPProfEnabled() bool { + return c.GetBool("debug.pprof") } -// SetConfigFile returns an error if viper was already initialized. -func (c *Configuration) SetConfigFile(path string) error { - if c.V != nil { - return errors.New("viper already initialized. Can't set config file") - } - c.configFile = path - return nil +// LoadConfiguration loads the configuration from the given file. +func LoadConfiguration(configFile string) Configuration { + cfg := &configuration{configFile: configFile, mu: sync.RWMutex{}} + cfg.initializeViper() + return cfg } -func (c *Configuration) ReadConfigFile(path string) error { +func (c *configuration) readConfigFile(path string) error { + c.mu.Lock() + defer c.mu.Unlock() file, err := os.Open(path) if err != nil { return err } - err = c.V.MergeConfig(file) + err = c.v.MergeConfig(file) return err } -func (c *Configuration) InitializeViper() { - // This method should not have any effects if Viper is already initialized. - if c.V != nil { +// initializeViper loads viper if not loaded already. +// This method should not have any effects if Viper is already initialized. +func (c *configuration) initializeViper() { + if c.v != nil { return } - c.V = viper.New() - c.V.SetConfigType("yaml") + c.v = viper.New() + c.v.SetConfigType("yaml") // Load defaults data, err := resources.Asset("go-centrifuge/build/configs/default_config.yaml") @@ -239,28 +344,28 @@ func (c *Configuration) InitializeViper() { log.Panicf("failed to load (go-centrifuge/build/configs/default_config.yaml): %s", err) } - err = c.V.ReadConfig(bytes.NewReader(data)) + err = c.v.ReadConfig(bytes.NewReader(data)) if err != nil { log.Panicf("Error reading from default configuration (go-centrifuge/build/configs/default_config.yaml): %s", err) } // Load user specified config if c.configFile != "" { log.Infof("Loading user specified config from %s", c.configFile) - err = c.ReadConfigFile(c.configFile) + err = c.readConfigFile(c.configFile) if err != nil { log.Panicf("Error reading config %s, %s", c.configFile, err) } } else { log.Info("No user config specified") } - c.V.AutomaticEnv() - c.V.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - c.V.SetEnvPrefix("CENT") + c.v.AutomaticEnv() + c.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + c.v.SetEnvPrefix("CENT") } -func Bootstrap(configFile string) { - Config = NewConfiguration(configFile) - Config.InitializeViper() +// SmartContractAddresses encapsulates the smart contract addresses ne +type SmartContractAddresses struct { + IdentityFactoryAddr, IdentityRegistryAddr, AnchorRepositoryAddr, PaymentObligationAddr string } // CreateConfigFile creates minimum config file with arguments @@ -269,10 +374,12 @@ func CreateConfigFile(args map[string]interface{}) (*viper.Viper, error) { accountKeyPath := args["accountKeyPath"].(string) accountPassword := args["accountPassword"].(string) network := args["network"].(string) - ethNodeUrl := args["ethNodeUrl"].(string) + ethNodeURL := args["ethNodeURL"].(string) bootstraps := args["bootstraps"].([]string) apiPort := args["apiPort"].(int64) p2pPort := args["p2pPort"].(int64) + p2pConnectTimeout := args["p2pConnectTimeout"].(string) + txPoolAccess := args["txpoolaccess"].(bool) if targetDataDir == "" { return nil, errors.New("targetDataDir not provided") @@ -282,7 +389,7 @@ func CreateConfigFile(args map[string]interface{}) (*viper.Viper, error) { } if _, err := os.Stat(accountKeyPath); os.IsNotExist(err) { - return nil, errors.New("Account Key Path does not exist") + return nil, errors.New("account Key Path does not exist") } bfile, err := ioutil.ReadFile(accountKeyPath) @@ -297,12 +404,17 @@ func CreateConfigFile(args map[string]interface{}) (*viper.Viper, error) { v := viper.New() v.SetConfigType("yaml") v.Set("storage.path", targetDataDir+"/db/centrifuge_data.leveldb") + v.Set("configStorage.path", targetDataDir+"/db/centrifuge_config_data.leveldb") v.Set("identityId", "") v.Set("centrifugeNetwork", network) v.Set("nodeHostname", "0.0.0.0") v.Set("nodePort", apiPort) v.Set("p2p.port", p2pPort) - v.Set("ethereum.nodeURL", ethNodeUrl) + if p2pConnectTimeout != "" { + v.Set("p2p.connectTimeout", p2pConnectTimeout) + } + v.Set("ethereum.nodeURL", ethNodeURL) + v.Set("ethereum.txPoolAccessEnabled", txPoolAccess) v.Set("ethereum.accounts.main.key", string(bfile)) v.Set("ethereum.accounts.main.password", accountPassword) v.Set("keys.p2p.privateKey", targetDataDir+"/p2p.key.pem") @@ -316,6 +428,13 @@ func CreateConfigFile(args map[string]interface{}) (*viper.Viper, error) { v.Set("networks."+network+".bootstrapPeers", bootstraps) } + if smartContractAddresses, ok := args["smartContractAddresses"].(*SmartContractAddresses); ok { + v.Set("networks."+network+".contractAddresses.identityFactory", smartContractAddresses.IdentityFactoryAddr) + v.Set("networks."+network+".contractAddresses.identityRegistry", smartContractAddresses.IdentityRegistryAddr) + v.Set("networks."+network+".contractAddresses.anchorRepository", smartContractAddresses.AnchorRepositoryAddr) + v.Set("networks."+network+".contractAddresses.paymentObligation", smartContractAddresses.PaymentObligationAddr) + } + v.SetConfigFile(targetDataDir + "/config.yaml") err = v.WriteConfig() diff --git a/config/configuration_test.go b/config/configuration_test.go index 3590391bc..dc3967a0c 100644 --- a/config/configuration_test.go +++ b/config/configuration_test.go @@ -22,14 +22,16 @@ func TestConfiguration_CreateConfigFile(t *testing.T) { assert.Nil(t, err, "err should be nil") data := map[string]interface{}{ - "targetDataDir": targetDir, - "accountKeyPath": accountKeyPath, - "accountPassword": "pwrd", - "network": "russianhill", - "ethNodeUrl": "ws://127.0.0.1:9546", - "bootstraps": []string{"/ip4/127.0.0.1/bootstrap1", "/ip4/127.0.0.1/bootstrap2"}, - "apiPort": int64(8082), - "p2pPort": int64(38202), + "targetDataDir": targetDir, + "accountKeyPath": accountKeyPath, + "accountPassword": "pwrd", + "network": "russianhill", + "ethNodeURL": "http://127.0.0.1:9545", + "bootstraps": []string{"/ip4/127.0.0.1/bootstrap1", "/ip4/127.0.0.1/bootstrap2"}, + "apiPort": int64(8082), + "p2pPort": int64(38202), + "txpoolaccess": false, + "p2pConnectTimeout": "", } v, err := CreateConfigFile(data) @@ -37,6 +39,7 @@ func TestConfiguration_CreateConfigFile(t *testing.T) { assert.Equal(t, data["p2pPort"].(int64), v.GetInt64("p2p.port"), "p2p port match") _, err = os.Stat(targetDir + "/config.yaml") assert.Nil(t, err, "must be nil, config file should be created") - + c := LoadConfiguration(v.ConfigFileUsed()) + assert.False(t, c.IsPProfEnabled(), "pprof is disabled by default") os.Remove(targetDir) } diff --git a/config/error.go b/config/error.go new file mode 100644 index 000000000..51140d84f --- /dev/null +++ b/config/error.go @@ -0,0 +1,11 @@ +package config + +import "github.com/centrifuge/go-centrifuge/errors" + +const ( + // ErrConfigBootstrap used as default error type + ErrConfigBootstrap = errors.Error("error when bootstrapping config") + + // ErrConfigFileBootstrapNotFound used when config file is not found + ErrConfigFileBootstrapNotFound = errors.Error("config file hasn't been provided") +) diff --git a/config/model.go b/config/model.go new file mode 100644 index 000000000..528b84f9f --- /dev/null +++ b/config/model.go @@ -0,0 +1,153 @@ +package config + +import ( + "encoding/json" + "math/big" + "reflect" + "time" +) + +// Model is an interface for both tenant and node config models +type Model interface { + // Get the ID of the document represented by this model + ID() ([]byte, error) + + //Returns the underlying type of the Model + Type() reflect.Type + + // JSON return the json representation of the model + JSON() ([]byte, error) + + // FromJSON initialize the model with a json + FromJSON(json []byte) error +} + +// KeyPair represents a key pair config +type KeyPair struct { + Pub, Priv string +} + +// NewKeyPair creates a KeyPair +func NewKeyPair(pub, priv string) KeyPair { + return KeyPair{Pub: pub, Priv: priv} +} + +// NodeConfig exposes configs specific to the node +type NodeConfig struct { + StoragePath string + P2PPort int + P2PExternalIP string + P2PConnectionTimeout time.Duration + ServerPort int + ServerAddress string + NumWorkers int + WorkerWaitTimeMS int + EthereumNodeURL string + EthereumContextReadWaitTimeout time.Duration + EthereumContextWaitTimeout time.Duration + EthereumIntervalRetry time.Duration + EthereumMaxRetries int + EthereumGasPrice *big.Int + EthereumGasLimit uint64 + TxPoolAccessEnabled bool + NetworkString string + BootstrapPeers []string + NetworkID uint32 + + // TODO what to do about contract addresses? +} + +// ID Gets the ID of the document represented by this model +func (nc *NodeConfig) ID() ([]byte, error) { + return []byte{}, nil +} + +// Type Returns the underlying type of the Model +func (nc *NodeConfig) Type() reflect.Type { + return reflect.TypeOf(nc) +} + +// JSON return the json representation of the model +func (nc *NodeConfig) JSON() ([]byte, error) { + return json.Marshal(nc) +} + +// FromJSON initialize the model with a json +func (nc *NodeConfig) FromJSON(data []byte) error { + return json.Unmarshal(data, nc) +} + +// NewNodeConfig creates a new NodeConfig instance with configs +func NewNodeConfig(config Configuration) *NodeConfig { + return &NodeConfig{ + StoragePath: config.GetStoragePath(), + P2PPort: config.GetP2PPort(), + P2PExternalIP: config.GetP2PExternalIP(), + P2PConnectionTimeout: config.GetP2PConnectionTimeout(), + ServerPort: config.GetServerPort(), + ServerAddress: config.GetServerAddress(), + NumWorkers: config.GetNumWorkers(), + WorkerWaitTimeMS: config.GetWorkerWaitTimeMS(), + EthereumNodeURL: config.GetEthereumNodeURL(), + EthereumContextReadWaitTimeout: config.GetEthereumContextReadWaitTimeout(), + EthereumContextWaitTimeout: config.GetEthereumContextWaitTimeout(), + EthereumIntervalRetry: config.GetEthereumIntervalRetry(), + EthereumMaxRetries: config.GetEthereumMaxRetries(), + EthereumGasPrice: config.GetEthereumGasPrice(), + EthereumGasLimit: config.GetEthereumGasLimit(), + TxPoolAccessEnabled: config.GetTxPoolAccessEnabled(), + NetworkString: config.GetNetworkString(), + BootstrapPeers: config.GetBootstrapPeers(), + NetworkID: config.GetNetworkID(), + } +} + +// TenantConfig exposes configs specific to a tenant in the node +type TenantConfig struct { + EthereumAccount *AccountConfig + EthereumDefaultAccountName string + ReceiveEventNotificationEndpoint string + IdentityID []byte + SigningKeyPair KeyPair + EthAuthKeyPair KeyPair +} + +// ID Get the ID of the document represented by this model +func (tc *TenantConfig) ID() ([]byte, error) { + return tc.IdentityID, nil +} + +// Type Returns the underlying type of the Model +func (tc *TenantConfig) Type() reflect.Type { + return reflect.TypeOf(tc) +} + +// JSON return the json representation of the model +func (tc *TenantConfig) JSON() ([]byte, error) { + return json.Marshal(tc) +} + +// FromJSON initialize the model with a json +func (tc *TenantConfig) FromJSON(data []byte) error { + return json.Unmarshal(data, tc) +} + +// NewTenantConfig creates a new TenantConfig instance with configs +func NewTenantConfig(ethAccountName string, config Configuration) (*TenantConfig, error) { + id, err := config.GetIdentityID() + if err != nil { + return nil, err + } + acc, err := config.GetEthereumAccount(ethAccountName) + if err != nil { + return nil, err + } + return &TenantConfig{ + EthereumAccount: acc, + EthereumDefaultAccountName: config.GetEthereumDefaultAccountName(), + IdentityID: id, + ReceiveEventNotificationEndpoint: config.GetReceiveEventNotificationEndpoint(), + SigningKeyPair: NewKeyPair(config.GetSigningKeyPair()), + EthAuthKeyPair: NewKeyPair(config.GetEthAuthKeyPair()), + }, nil +} diff --git a/config/model_test.go b/config/model_test.go new file mode 100644 index 000000000..b7b52a56c --- /dev/null +++ b/config/model_test.go @@ -0,0 +1,206 @@ +// +build unit + +package config + +import ( + "testing" + + "math/big" + "time" + + "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +type mockConfig struct { + Configuration + mock.Mock +} + +func (m *mockConfig) GetStoragePath() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetConfigStoragePath() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetP2PPort() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *mockConfig) GetP2PExternalIP() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetP2PConnectionTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *mockConfig) GetReceiveEventNotificationEndpoint() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetServerPort() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *mockConfig) GetServerAddress() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetNumWorkers() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *mockConfig) GetWorkerWaitTimeMS() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *mockConfig) GetEthereumNodeURL() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetEthereumContextReadWaitTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *mockConfig) GetEthereumContextWaitTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *mockConfig) GetEthereumIntervalRetry() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *mockConfig) GetEthereumMaxRetries() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *mockConfig) GetEthereumGasPrice() *big.Int { + args := m.Called() + return args.Get(0).(*big.Int) +} + +func (m *mockConfig) GetEthereumGasLimit() uint64 { + args := m.Called() + return args.Get(0).(uint64) +} + +func (m *mockConfig) GetEthereumDefaultAccountName() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetEthereumAccount(accountName string) (account *AccountConfig, err error) { + args := m.Called(accountName) + return args.Get(0).(*AccountConfig), args.Error(1) +} + +func (m *mockConfig) GetTxPoolAccessEnabled() bool { + args := m.Called() + return args.Get(0).(bool) +} + +func (m *mockConfig) GetNetworkString() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetNetworkKey(k string) string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetContractAddressString(address string) string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *mockConfig) GetContractAddress(address string) common.Address { + args := m.Called() + return args.Get(0).(common.Address) +} + +func (m *mockConfig) GetBootstrapPeers() []string { + args := m.Called() + return args.Get(0).([]string) +} + +func (m *mockConfig) GetNetworkID() uint32 { + args := m.Called() + return args.Get(0).(uint32) +} + +func (m *mockConfig) GetIdentityID() ([]byte, error) { + args := m.Called() + return args.Get(0).([]byte), args.Error(1) +} + +func (m *mockConfig) GetSigningKeyPair() (pub, priv string) { + args := m.Called() + return args.Get(0).(string), args.Get(1).(string) +} + +func (m *mockConfig) GetEthAuthKeyPair() (pub, priv string) { + args := m.Called() + return args.Get(0).(string), args.Get(1).(string) +} + +func TestNewNodeConfig(t *testing.T) { + c := &mockConfig{} + c.On("GetStoragePath").Return("dummyStorage").Once() + c.On("GetP2PPort").Return(30000).Once() + c.On("GetP2PExternalIP").Return("ip").Once() + c.On("GetP2PConnectionTimeout").Return(time.Second).Once() + + c.On("GetServerPort").Return(8080).Once() + c.On("GetServerAddress").Return("dummyServer").Once() + c.On("GetNumWorkers").Return(2).Once() + c.On("GetWorkerWaitTimeMS").Return(1).Once() + c.On("GetEthereumNodeURL").Return("dummyNode").Once() + + c.On("GetEthereumContextReadWaitTimeout").Return(time.Second).Once() + c.On("GetEthereumContextWaitTimeout").Return(time.Second).Once() + c.On("GetEthereumIntervalRetry").Return(time.Second).Once() + c.On("GetEthereumMaxRetries").Return(1).Once() + c.On("GetEthereumGasPrice").Return(big.NewInt(1)).Once() + + c.On("GetEthereumGasLimit").Return(uint64(100)).Once() + c.On("GetTxPoolAccessEnabled").Return(true).Once() + c.On("GetNetworkString").Return("somehill").Once() + c.On("GetBootstrapPeers").Return([]string{"p1", "p2"}).Once() + + c.On("GetNetworkID").Return(uint32(1)).Once() + NewNodeConfig(c) + + c.AssertExpectations(t) +} + +func TestNewTenantConfig(t *testing.T) { + c := &mockConfig{} + c.On("GetEthereumAccount", "name").Return(&AccountConfig{}, nil).Once() + c.On("GetEthereumDefaultAccountName").Return("dummyAcc").Once() + c.On("GetReceiveEventNotificationEndpoint").Return("dummyNotifier").Once() + c.On("GetIdentityID").Return(utils.RandomSlice(6), nil).Once() + c.On("GetSigningKeyPair").Return("pub", "priv").Once() + c.On("GetEthAuthKeyPair").Return("pub", "priv").Once() + NewTenantConfig("name", c) + c.AssertExpectations(t) +} diff --git a/config/repository.go b/config/repository.go new file mode 100644 index 000000000..ca32baabc --- /dev/null +++ b/config/repository.go @@ -0,0 +1,271 @@ +package config + +import ( + "encoding/json" + "fmt" + "reflect" + "sync" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/util" +) + +const ( + configPrefix string = "config" + tenantPrefix string = "tenant-" +) + +// Repository defines the required methods for the config repository. +type Repository interface { + // Get returns the tenant config Model associated with tenant ID + GetTenant(id []byte) (Model, error) + + // GetConfig returns the node config model + GetConfig() (Model, error) + + // GetAllTenants returns a list of all tenant models in the config DB + GetAllTenants() ([]Model, error) + + // Create creates the tenant config model if not present in the DB. + // should error out if the config exists. + CreateTenant(id []byte, model Model) error + + // Create creates the node config model if not present in the DB. + // should error out if the config exists. + CreateConfig(model Model) error + + // Update strictly updates the tenant config model. + // Will error out when the config model doesn't exist in the DB. + UpdateTenant(id []byte, model Model) error + + // Update strictly updates the node config model. + // Will error out when the config model doesn't exist in the DB. + UpdateConfig(model Model) error + + // Delete deletes tenant config + // Will not error out when config model doesn't exists in DB + DeleteTenant(id []byte) error + + // Delete deletes node config + // Will not error out when config model doesn't exists in DB + DeleteConfig() error + + // Register registers the model so that the DB can return the config without knowing the type + Register(model Model) +} + +// levelDBRepo implements Repository using LevelDB as storage layer +type levelDBRepo struct { + db *leveldb.DB + models map[string]reflect.Type + mu sync.RWMutex // to protect the models +} + +// value is an internal representation of how levelDb stores the model. +type value struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` +} + +// NewLevelDBRepository returns levelDb implementation of Repository +func NewLevelDBRepository(db *leveldb.DB) Repository { + return &levelDBRepo{ + db: db, + models: make(map[string]reflect.Type), + } +} + +func (l *levelDBRepo) getTenantKey(id []byte) []byte { + return append([]byte(tenantPrefix), id...) +} + +func (l *levelDBRepo) getConfigKey() []byte { + return []byte(configPrefix) +} + +// getModel returns a new instance of the type mt. +func (l *levelDBRepo) getModel(mt string) (Model, error) { + tp, ok := l.models[mt] + if !ok { + return nil, fmt.Errorf("type %s not registered", mt) + } + + return reflect.New(tp).Interface().(Model), nil +} + +func (l *levelDBRepo) GetTenant(id []byte) (Model, error) { + key := l.getTenantKey(id) + return l.get(key) +} + +func (l *levelDBRepo) GetConfig() (Model, error) { + key := l.getConfigKey() + return l.get(key) +} + +func (l *levelDBRepo) parseModel(data []byte) (Model, error) { + v := new(value) + err := json.Unmarshal(data, v) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal value: %v", err) + } + + nm, err := l.getModel(v.Type) + if err != nil { + return nil, fmt.Errorf("failed to get model type: %v", err) + } + + err = nm.FromJSON([]byte(v.Data)) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal to model: %v", err) + } + + return nm, nil +} + +// Get returns the model associated with ID +func (l *levelDBRepo) get(id []byte) (Model, error) { + l.mu.RLock() + defer l.mu.RUnlock() + data, err := l.db.Get(id, nil) + if err != nil { + return nil, fmt.Errorf("config missing: %v", err) + } + + return l.parseModel(data) +} + +// GetAllTenants iterates over all tenant entries in DB and returns a list of Models +// If an error occur reading a tenant, throws a warning and continue +func (l *levelDBRepo) GetAllTenants() ([]Model, error) { + var models []Model + l.mu.RLock() + defer l.mu.RUnlock() + iter := l.db.NewIterator(util.BytesPrefix([]byte(tenantPrefix)), nil) + for iter.Next() { + data := iter.Value() + model, err := l.parseModel(data) + if err != nil { + log.Warningf("Error parsing tenant: %v", err) + continue + } + models = append(models, model) + } + iter.Release() + return models, iter.Error() +} + +// save stores the model. +func (l *levelDBRepo) save(id []byte, model Model) error { + data, err := model.JSON() + if err != nil { + return fmt.Errorf("failed to marshall model: %v", err) + } + + tp := getTypeIndirect(model.Type()) + v := value{ + Type: tp.String(), + Data: json.RawMessage(data), + } + + data, err = json.Marshal(v) + if err != nil { + return fmt.Errorf("failed to marshall value: %v", err) + } + + err = l.db.Put(id, data, nil) + if err != nil { + return fmt.Errorf("failed to save model to DB: %v", err) + } + + return nil +} + +// Exists returns true if the id exists. +func (l *levelDBRepo) exists(id []byte) bool { + res, err := l.db.Has(id, nil) + // TODO check this + if err != nil { + return false + } + + return res +} + +// Create creates the tenant config model if not present in the DB. +// should error out if the config exists. +func (l *levelDBRepo) CreateTenant(id []byte, model Model) error { + key := l.getTenantKey(id) + return l.create(key, model) +} + +// Create creates the node config model if not present in the DB. +// should error out if the config exists. +func (l *levelDBRepo) CreateConfig(model Model) error { + key := l.getConfigKey() + return l.create(key, model) +} + +// Create stores the model to the DB. +// Errors out if the model already exists. +func (l *levelDBRepo) create(id []byte, model Model) error { + if l.exists(id) { + return fmt.Errorf("model already exists") + } + + return l.save(id, model) +} + +// Update strictly updates the tenant config model. +// Will error out when the config model doesn't exist in the DB. +func (l *levelDBRepo) UpdateTenant(id []byte, model Model) error { + key := l.getTenantKey(id) + return l.update(key, model) +} + +// Update strictly updates the node config model. +// Will error out when the config model doesn't exist in the DB. +func (l *levelDBRepo) UpdateConfig(model Model) error { + key := l.getConfigKey() + return l.update(key, model) +} + +// Update overwrites the value at tenantID+id. +// Errors out if model doesn't exist +func (l *levelDBRepo) update(id []byte, model Model) error { + if !l.exists(id) { + return fmt.Errorf("model doesn't exist") + } + + return l.save(id, model) +} + +// Delete deletes tenant config +// Will not error out when config model doesn't exists in DB +func (l *levelDBRepo) DeleteTenant(id []byte) error { + key := l.getTenantKey(id) + return l.db.Delete(key, nil) +} + +func (l *levelDBRepo) DeleteConfig() error { + key := l.getConfigKey() + return l.db.Delete(key, nil) +} + +// Register registers the model for type less operations. +// Same type names will be overwritten. +func (l *levelDBRepo) Register(model Model) { + l.mu.Lock() + defer l.mu.Unlock() + tp := getTypeIndirect(model.Type()) + l.models[tp.String()] = tp +} + +// getTypeIndirect returns the type of the model without pointers. +func getTypeIndirect(tp reflect.Type) reflect.Type { + if tp.Kind() == reflect.Ptr { + return getTypeIndirect(tp.Elem()) + } + + return tp +} diff --git a/config/repository_test.go b/config/repository_test.go new file mode 100644 index 000000000..c5244ee83 --- /dev/null +++ b/config/repository_test.go @@ -0,0 +1,166 @@ +// +build unit + +package config + +import ( + "os" + "reflect" + "testing" + + "github.com/syndtr/goleveldb/leveldb" + + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/stretchr/testify/assert" +) + +func getRandomStorage() (*leveldb.DB, error) { + return storage.NewLevelDBStorage(storage.GetRandomTestStoragePath()) +} + +func TestMain(m *testing.M) { + result := m.Run() + os.Exit(result) +} + +func TestNewLevelDBRepository(t *testing.T) { + testStorage, _ := getRandomStorage() + repo := NewLevelDBRepository(testStorage) + assert.NotNil(t, repo) +} + +func TestUnregisteredModel(t *testing.T) { + testStorage, _ := getRandomStorage() + repo := NewLevelDBRepository(testStorage) + assert.NotNil(t, repo) + id := utils.RandomSlice(32) + newTenant := &TenantConfig{ + IdentityID: id, + EthereumDefaultAccountName: "main", + } + err := repo.CreateTenant(id, newTenant) + assert.Nil(t, err) + + // Error on non registered model + _, err = repo.GetTenant(id) + assert.NotNil(t, err) + + repo.Register(&TenantConfig{}) + + _, err = repo.GetTenant(id) + assert.Nil(t, err) +} + +func TestTenantOperations(t *testing.T) { + testStorage, _ := getRandomStorage() + repo := NewLevelDBRepository(testStorage) + assert.NotNil(t, repo) + id := utils.RandomSlice(32) + newTenant := &TenantConfig{ + IdentityID: id, + EthereumDefaultAccountName: "main", + } + repo.Register(&TenantConfig{}) + err := repo.CreateTenant(id, newTenant) + assert.Nil(t, err) + + // Create tenant already exist + err = repo.CreateTenant(id, newTenant) + assert.NotNil(t, err) + + readModel, err := repo.GetTenant(id) + assert.Nil(t, err) + readTenant := readModel.(*TenantConfig) + assert.Equal(t, reflect.TypeOf(newTenant), readTenant.Type()) + assert.Equal(t, newTenant.IdentityID, readTenant.IdentityID) + + // Update tenant + newTenant.EthereumDefaultAccountName = "secondary" + err = repo.UpdateTenant(id, newTenant) + assert.Nil(t, err) + + // Update tenant does not exist + newId := utils.RandomSlice(32) + err = repo.UpdateTenant(newId, newTenant) + assert.NotNil(t, err) + + // Delete tenant + err = repo.DeleteTenant(id) + assert.Nil(t, err) + _, err = repo.GetTenant(id) + assert.NotNil(t, err) +} + +func TestConfigOperations(t *testing.T) { + testStorage, _ := getRandomStorage() + repo := NewLevelDBRepository(testStorage) + assert.NotNil(t, repo) + newConfig := &NodeConfig{ + NetworkID: 4, + } + repo.Register(&NodeConfig{}) + err := repo.CreateConfig(newConfig) + assert.Nil(t, err) + + // Create config already exist + err = repo.CreateConfig(newConfig) + assert.NotNil(t, err) + + readModel, err := repo.GetConfig() + readDoc := readModel.(*NodeConfig) + assert.Nil(t, err) + assert.Equal(t, reflect.TypeOf(newConfig), readDoc.Type()) + assert.Equal(t, newConfig.NetworkID, readDoc.NetworkID) + + // Update config + newConfig.NetworkID = 42 + err = repo.UpdateConfig(newConfig) + assert.Nil(t, err) + + // Delete config + err = repo.DeleteConfig() + assert.Nil(t, err) + _, err = repo.GetConfig() + assert.NotNil(t, err) + + // Update config does not exist + err = repo.UpdateConfig(newConfig) + assert.NotNil(t, err) +} + +func TestLevelDBRepo_GetAllTenants(t *testing.T) { + testStorage, _ := getRandomStorage() + repo := NewLevelDBRepository(testStorage) + assert.NotNil(t, repo) + repo.Register(&TenantConfig{}) + ids := [][]byte{utils.RandomSlice(32), utils.RandomSlice(32), utils.RandomSlice(32)} + ten1 := &TenantConfig{ + IdentityID: ids[0], + EthereumDefaultAccountName: "main", + } + ten2 := &TenantConfig{ + IdentityID: ids[1], + EthereumDefaultAccountName: "main", + } + ten3 := &TenantConfig{ + IdentityID: ids[2], + EthereumDefaultAccountName: "main", + } + + err := repo.CreateTenant(ids[0], ten1) + assert.Nil(t, err) + err = repo.CreateTenant(ids[1], ten2) + assert.Nil(t, err) + err = repo.CreateTenant(ids[2], ten3) + assert.Nil(t, err) + + tenants, err := repo.GetAllTenants() + assert.Nil(t, err) + assert.Equal(t, 3, len(tenants)) + t0Id, _ := tenants[0].ID() + t1Id, _ := tenants[1].ID() + t2Id, _ := tenants[2].ID() + assert.Contains(t, ids, t0Id) + assert.Contains(t, ids, t1Id) + assert.Contains(t, ids, t2Id) +} diff --git a/config/test_bootstrapper.go b/config/test_bootstrapper.go index 211c9a9a0..59c9b90da 100644 --- a/config/test_bootstrapper.go +++ b/config/test_bootstrapper.go @@ -23,13 +23,12 @@ func (*Bootstrapper) TestBootstrap(context map[string]interface{}) error { log.Fatal("Current working dir is not in `go-centrifuge`") } } - Config = NewConfiguration(fmt.Sprintf("%s/build/configs/testing_config.yaml", match)) - Config.InitializeViper() - context[bootstrap.BootstrappedConfig] = Config + + context[bootstrap.BootstrappedConfig] = LoadConfiguration(fmt.Sprintf("%s/build/configs/testing_config.yaml", match)) + return nil } func (b *Bootstrapper) TestTearDown() error { - Config = nil return nil } diff --git a/context/bootstrapper.go b/context/bootstrapper.go index 9b56ee6f2..a0274691c 100644 --- a/context/bootstrapper.go +++ b/context/bootstrapper.go @@ -2,14 +2,17 @@ package context import ( "github.com/centrifuge/go-centrifuge/anchors" + "github.com/centrifuge/go-centrifuge/api" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/nft" "github.com/centrifuge/go-centrifuge/node" + "github.com/centrifuge/go-centrifuge/p2p" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/storage" "github.com/centrifuge/go-centrifuge/version" @@ -18,31 +21,38 @@ import ( var log = logging.Logger("context") +// MainBootstrapper holds all the bootstrapper implementations type MainBootstrapper struct { Bootstrappers []bootstrap.Bootstrapper } +// PopulateBaseBootstrappers adds all the bootstrapper implementations to MainBootstrapper func (m *MainBootstrapper) PopulateBaseBootstrappers() { m.Bootstrappers = []bootstrap.Bootstrapper{ &version.Bootstrapper{}, &config.Bootstrapper{}, &storage.Bootstrapper{}, - ðereum.Bootstrapper{}, + ethereum.Bootstrapper{}, + &queue.Bootstrapper{}, &anchors.Bootstrapper{}, &identity.Bootstrapper{}, + documents.Bootstrapper{}, + p2p.Bootstrapper{}, + api.Bootstrapper{}, &invoice.Bootstrapper{}, &purchaseorder.Bootstrapper{}, &nft.Bootstrapper{}, - &queue.Bootstrapper{}, } } +// PopulateRunBootstrappers adds blocking Node bootstrapper at the end. +// Note: Node bootstrapper must be the last bootstrapper to be invoked as it won't return until node is shutdown func (m *MainBootstrapper) PopulateRunBootstrappers() { m.PopulateBaseBootstrappers() - // NODE BOOTSTRAPPER MUST BE THE LAST BOOTSTRAPPER TO BE INVOKED AS IT WON'T RETURN UNTIL NODE IS SHUTDOWN m.Bootstrappers = append(m.Bootstrappers, &node.Bootstrapper{}) } +// Bootstrap runs all the loaded bootstrapper implementations. func (m *MainBootstrapper) Bootstrap(context map[string]interface{}) error { for _, b := range m.Bootstrappers { err := b.Bootstrap(context) diff --git a/context/testingbootstrap/testing_bootstrap.go b/context/testingbootstrap/testing_bootstrap.go index 85fcf41df..92caff014 100644 --- a/context/testingbootstrap/testing_bootstrap.go +++ b/context/testingbootstrap/testing_bootstrap.go @@ -7,11 +7,13 @@ import ( "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/context/testlogging" + "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" "github.com/centrifuge/go-centrifuge/documents/purchaseorder" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/nft" + "github.com/centrifuge/go-centrifuge/p2p" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/storage" logging "github.com/ipfs/go-log" @@ -23,24 +25,29 @@ var bootstappers = []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, &storage.Bootstrapper{}, - ðereum.Bootstrapper{}, - &anchors.Bootstrapper{}, + ethereum.Bootstrapper{}, + &queue.Bootstrapper{}, + anchors.Bootstrapper{}, &identity.Bootstrapper{}, + documents.Bootstrapper{}, + p2p.Bootstrapper{}, &invoice.Bootstrapper{}, &purchaseorder.Bootstrapper{}, &nft.Bootstrapper{}, - &queue.Bootstrapper{}, + &queue.Starter{}, } -func TestFunctionalEthereumBootstrap() { - contextval := map[string]interface{}{} +func TestFunctionalEthereumBootstrap() map[string]interface{} { + ctx := map[string]interface{}{} for _, b := range bootstappers { - err := b.TestBootstrap(contextval) + err := b.TestBootstrap(ctx) if err != nil { log.Error("Error encountered while bootstrapping", err) panic(err) } } + + return ctx } func TestFunctionalEthereumTearDown() { for _, b := range bootstappers { diff --git a/coredocument/coredocument.go b/coredocument/coredocument.go index 80d160910..d3bb495d9 100644 --- a/coredocument/coredocument.go +++ b/coredocument/coredocument.go @@ -6,8 +6,8 @@ import ( "strings" "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/keytools/ed25519" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs" "github.com/centrifuge/precise-proofs/proofs/proto" @@ -62,7 +62,7 @@ func CalculateSigningRoot(doc *coredocumentpb.CoreDocument) error { // CalculateDocumentRoot calculates the document root of the core document func CalculateDocumentRoot(document *coredocumentpb.CoreDocument) error { if len(document.SigningRoot) != 32 { - return fmt.Errorf("signing root invalid") + return errors.New("signing root invalid") } tree, err := GetDocumentRootTree(document) @@ -81,27 +81,28 @@ func GetDocumentRootTree(document *coredocumentpb.CoreDocument) (tree *proofs.Do tree = &t // The first leave added is the signing_root - err = tree.AddLeaf(proofs.LeafNode{Hash: document.SigningRoot, Hashed: true, Property: "signing_root"}) + err = tree.AddLeaf(proofs.LeafNode{Hash: document.SigningRoot, Hashed: true, Property: proofs.NewProperty("signing_root")}) if err != nil { return nil, err } // For every signature we create a LeafNode + sigProperty := proofs.NewProperty("signatures") sigLeafList := make([]proofs.LeafNode, len(document.Signatures)+1) sigLengthNode := proofs.LeafNode{ - Property: "signatures.length", + Property: sigProperty.LengthProp(), Salt: make([]byte, 32), Value: fmt.Sprintf("%d", len(document.Signatures)), } - sigLengthNode.HashNode(h) + sigLengthNode.HashNode(h, false) sigLeafList[0] = sigLengthNode for i, sig := range document.Signatures { payload := sha256.Sum256(append(sig.EntityId, append(sig.PublicKey, sig.Signature...)...)) leaf := proofs.LeafNode{ Hash: payload[:], Hashed: true, - Property: fmt.Sprintf("signatures[%d]", i), + Property: sigProperty.ElemProp(proofs.FieldNum(i)), } - leaf.HashNode(h) + leaf.HashNode(h, false) sigLeafList[i+1] = leaf } err = tree.AddLeaves(sigLeafList) @@ -126,15 +127,15 @@ func GetDocumentSigningTree(document *coredocumentpb.CoreDocument) (tree *proofs } if document.EmbeddedData == nil { - return nil, fmt.Errorf("EmbeddedData cannot be nil when generating signing tree") + return nil, errors.New("EmbeddedData cannot be nil when generating signing tree") } // Adding document type as it is an excluded field in the tree documentTypeNode := proofs.LeafNode{ - Property: "document_type", + Property: proofs.NewProperty("document_type"), Salt: make([]byte, 32), Value: document.EmbeddedData.TypeUrl, } - documentTypeNode.HashNode(h) + documentTypeNode.HashNode(h, false) err = tree.AddLeaf(documentTypeNode) if err != nil { return nil, err @@ -157,22 +158,22 @@ func PrepareNewVersion(oldCD coredocumentpb.CoreDocument, collaborators []string } if oldCD.DocumentIdentifier == nil { - return nil, fmt.Errorf("coredocument.DocumentIdentifier is nil") + return nil, errors.New("coredocument.DocumentIdentifier is nil") } newCD.DocumentIdentifier = oldCD.DocumentIdentifier if oldCD.CurrentVersion == nil { - return nil, fmt.Errorf("coredocument.CurrentVersion is nil") + return nil, errors.New("coredocument.CurrentVersion is nil") } newCD.PreviousVersion = oldCD.CurrentVersion if oldCD.NextVersion == nil { - return nil, fmt.Errorf("coredocument.NextVersion is nil") + return nil, errors.New("coredocument.NextVersion is nil") } newCD.CurrentVersion = oldCD.NextVersion newCD.NextVersion = utils.RandomSlice(32) if oldCD.DocumentRoot == nil { - return nil, fmt.Errorf("coredocument.DocumentRoot is nil") + return nil, errors.New("coredocument.DocumentRoot is nil") } newCD.PreviousRoot = oldCD.DocumentRoot return newCD, nil @@ -194,7 +195,7 @@ func NewWithCollaborators(collaborators []string) (*coredocumentpb.CoreDocument, cd := New() ids, err := identity.CentIDsFromStrings(collaborators) if err != nil { - return nil, fmt.Errorf("failed to decode collaborator: %v", err) + return nil, errors.New("failed to decode collaborator: %v", err) } for i := range ids { @@ -209,16 +210,16 @@ func NewWithCollaborators(collaborators []string) (*coredocumentpb.CoreDocument, return cd, nil } -// GetExternalCollaborators returns collaborators of a document without the own centID -func GetExternalCollaborators(doc *coredocumentpb.CoreDocument) ([][]byte, error) { +// GetExternalCollaborators returns collaborators of a document without the own centID. +func GetExternalCollaborators(selfCentID identity.CentID, doc *coredocumentpb.CoreDocument) ([][]byte, error) { var collabs [][]byte - idConfig, err := ed25519.GetIDConfig() - if err != nil { - return nil, fmt.Errorf("failed to decode collaborator: %v", err) - } for _, collab := range doc.Collaborators { - if !utils.IsSameByteSlice(collab, idConfig.ID) { + collabID, err := identity.ToCentID(collab) + if err != nil { + return nil, errors.New("failed to convert to CentID: %v", err) + } + if !selfCentID.Equal(collabID) { collabs = append(collabs, collab) } } @@ -231,7 +232,7 @@ func FillSalts(doc *coredocumentpb.CoreDocument) error { salts := &coredocumentpb.CoreDocumentSalts{} err := proofs.FillSalts(doc, salts) if err != nil { - return fmt.Errorf("failed to fill coredocument salts: %v", err) + return errors.New("failed to fill coredocument salts: %v", err) } doc.CoredocumentSalts = salts @@ -242,15 +243,15 @@ func FillSalts(doc *coredocumentpb.CoreDocument) error { func GetTypeURL(coreDocument *coredocumentpb.CoreDocument) (string, error) { if coreDocument == nil { - return "", fmt.Errorf("core document is nil") + return "", errors.New("core document is nil") } if coreDocument.EmbeddedData == nil { - return "", fmt.Errorf("core document doesn't have embedded data") + return "", errors.New("core document doesn't have embedded data") } if coreDocument.EmbeddedData.TypeUrl == "" { - return "", fmt.Errorf("typeUrl not set properly") + return "", errors.New("typeUrl not set properly") } return coreDocument.EmbeddedData.TypeUrl, nil } @@ -259,17 +260,17 @@ func GetTypeURL(coreDocument *coredocumentpb.CoreDocument) (string, error) { func CreateProofs(dataTree *proofs.DocumentTree, coreDoc *coredocumentpb.CoreDocument, fields []string) (proofs []*proofspb.Proof, err error) { dataRootHashes, err := getDataProofHashes(coreDoc) if err != nil { - return nil, fmt.Errorf("createProofs error %v", err) + return nil, errors.New("createProofs error %v", err) } signingRootHashes, err := getSigningProofHashes(coreDoc) if err != nil { - return nil, fmt.Errorf("createProofs error %v", err) + return nil, errors.New("createProofs error %v", err) } cdtree, err := GetDocumentSigningTree(coreDoc) if err != nil { - return nil, fmt.Errorf("createProofs error %v", err) + return nil, errors.New("createProofs error %v", err) } // We support fields that belong to different document trees, as we do not prepend a tree prefix to the field, the approach @@ -281,15 +282,16 @@ func CreateProofs(dataTree *proofs.DocumentTree, coreDoc *coredocumentpb.CoreDoc if strings.Contains(err.Error(), "No such field") { proof, err = cdtree.CreateProof(field) if err != nil { - return nil, fmt.Errorf("createProofs error %v", err) + return nil, errors.New("createProofs error %v", err) } rootHashes = signingRootHashes } else { - return nil, fmt.Errorf("createProofs error %v", err) + return nil, errors.New("createProofs error %v", err) } } proof.SortedHashes = append(proof.SortedHashes, rootHashes...) proofs = append(proofs, &proof) } - return + + return proofs, nil } diff --git a/coredocument/coredocument_test.go b/coredocument/coredocument_test.go index 4c01639f9..bfccd4045 100644 --- a/coredocument/coredocument_test.go +++ b/coredocument/coredocument_test.go @@ -3,13 +3,18 @@ package coredocument import ( + "context" "crypto/sha256" + "flag" + "os" "testing" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/header" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs" "github.com/ethereum/go-ethereum/common/hexutil" @@ -23,12 +28,27 @@ var ( id3 = utils.RandomSlice(32) id4 = utils.RandomSlice(32) id5 = utils.RandomSlice(32) - - centID = utils.RandomSlice(identity.CentIDLength) - 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} ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + +func TestMain(m *testing.M) { + ibootstappers := []bootstrap.TestBootstrapper{ + &config.Bootstrapper{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + flag.Parse() + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} + func TestGetSigningProofHashes(t *testing.T) { docAny := &any.Any{ TypeUrl: documenttypes.InvoiceDataTypeUrl, @@ -127,7 +147,7 @@ func TestGetDocumentRootTree(t *testing.T) { } func TestGetTypeUrl(t *testing.T) { - coreDocument := testingutils.GenerateCoreDocument() + coreDocument := testingcoredocument.GenerateCoreDocument() documentType, err := GetTypeURL(coreDocument) assert.Nil(t, err, "should not throw an error because coreDocument has a type") @@ -224,8 +244,23 @@ func TestGetExternalCollaborators(t *testing.T) { c := []string{hexutil.Encode(c1), hexutil.Encode(c2)} cd, err := NewWithCollaborators(c) assert.Equal(t, [][]byte{c1, c2}, cd.Collaborators) - collaborators, err := GetExternalCollaborators(cd) + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) + collaborators, err := GetExternalCollaborators(ctxh.Self().ID, cd) assert.Nil(t, err) assert.NotNil(t, collaborators) assert.Equal(t, [][]byte{c1, c2}, collaborators) } + +func TestGetExternalCollaborators_WrongIDFormat(t *testing.T) { + c1 := utils.RandomSlice(6) + c2 := utils.RandomSlice(6) + c := []string{hexutil.Encode(c1), hexutil.Encode(c2)} + cd, err := NewWithCollaborators(c) + assert.Equal(t, [][]byte{c1, c2}, cd.Collaborators) + cd.Collaborators[1] = utils.RandomSlice(5) + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) + _, err = GetExternalCollaborators(ctxh.Self().ID, cd) + assert.NotNil(t, err) +} diff --git a/coredocument/processor.go b/coredocument/processor.go new file mode 100644 index 000000000..fc9b8452d --- /dev/null +++ b/coredocument/processor.go @@ -0,0 +1,274 @@ +package coredocument + +import ( + "fmt" + + "context" + "time" + + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" + "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" + "github.com/centrifuge/go-centrifuge/anchors" + "github.com/centrifuge/go-centrifuge/centerrors" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/keytools/secp256k1" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/centrifuge/go-centrifuge/version" + logging "github.com/ipfs/go-log" +) + +var log = logging.Logger("coredocument") + +// Config defines required methods required for the coredocument package. +type Config interface { + GetNetworkID() uint32 + GetIdentityID() ([]byte, error) + GetP2PConnectionTimeout() time.Duration +} + +// Processor identifies an implementation, which can do a bunch of things with a CoreDocument. +// E.g. send, anchor, etc. +type Processor interface { + Send(ctx *header.ContextHeader, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) + PrepareForSignatureRequests(ctx *header.ContextHeader, model documents.Model) error + RequestSignatures(ctx *header.ContextHeader, model documents.Model) error + PrepareForAnchoring(model documents.Model) error + AnchorDocument(ctx *header.ContextHeader, model documents.Model) error + SendDocument(ctx *header.ContextHeader, model documents.Model) error +} + +// client defines the methods for p2pclient +// we redefined it here so that we can avoid cyclic dependencies with p2p +type client interface { + OpenClient(target string) (p2ppb.P2PServiceClient, error) + GetSignaturesForDocument(ctx *header.ContextHeader, identityService identity.Service, doc *coredocumentpb.CoreDocument) error +} + +// defaultProcessor implements Processor interface +type defaultProcessor struct { + identityService identity.Service + p2pClient client + anchorRepository anchors.AnchorRepository + config Config +} + +// DefaultProcessor returns the default implementation of CoreDocument Processor +func DefaultProcessor(idService identity.Service, p2pClient client, repository anchors.AnchorRepository, config Config) Processor { + return defaultProcessor{ + identityService: idService, + p2pClient: p2pClient, + anchorRepository: repository, + config: config, + } +} + +// Send sends the given defaultProcessor to the given recipient on the P2P layer +func (dp defaultProcessor) Send(ctx *header.ContextHeader, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) { + if coreDocument == nil { + return centerrors.NilError(coreDocument) + } + + log.Infof("sending coredocument %x to recipient %x", coreDocument.DocumentIdentifier, recipient) + id, err := dp.identityService.LookupIdentityForID(recipient) + if err != nil { + return centerrors.Wrap(err, "error fetching receiver identity") + } + + lastB58Key, err := id.CurrentP2PKey() + if err != nil { + return centerrors.Wrap(err, "error fetching p2p key") + } + + log.Infof("Sending Document to CentID [%v] with Key [%v]\n", recipient, lastB58Key) + clientWithProtocol := fmt.Sprintf("/ipfs/%s", lastB58Key) + client, err := dp.p2pClient.OpenClient(clientWithProtocol) + if err != nil { + return errors.New("failed to open client: %v", err) + } + + log.Infof("Done opening connection against [%s]\n", lastB58Key) + idConfig := ctx.Self() + centIDBytes := idConfig.ID[:] + p2pheader := &p2ppb.CentrifugeHeader{ + SenderCentrifugeId: centIDBytes, + CentNodeVersion: version.GetVersion().String(), + NetworkIdentifier: dp.config.GetNetworkID(), + } + + c, _ := context.WithTimeout(ctx.Context(), dp.config.GetP2PConnectionTimeout()) + resp, err := client.SendAnchoredDocument(c, &p2ppb.AnchorDocumentRequest{Document: coreDocument, Header: p2pheader}) + if err != nil || !resp.Accepted { + return centerrors.Wrap(err, "failed to send document to the node") + } + + return nil +} + +// PrepareForSignatureRequests gets the core document from the model, and adds the node's own signature +func (dp defaultProcessor) PrepareForSignatureRequests(ctx *header.ContextHeader, model documents.Model) error { + cd, err := model.PackCoreDocument() + if err != nil { + return errors.New("failed to pack core document: %v", err) + } + + // calculate the signing root + err = CalculateSigningRoot(cd) + if err != nil { + return errors.New("failed to calculate signing root: %v", err) + } + + sig := identity.Sign(ctx.Self(), identity.KeyPurposeSigning, cd.SigningRoot) + cd.Signatures = append(cd.Signatures, sig) + + err = model.UnpackCoreDocument(cd) + if err != nil { + return errors.New("failed to unpack the core document: %v", err) + } + + return nil +} + +// RequestSignatures gets the core document from the model, validates pre signature requirements, +// collects signatures, and validates the signatures, +func (dp defaultProcessor) RequestSignatures(ctx *header.ContextHeader, model documents.Model) error { + cd, err := model.PackCoreDocument() + if err != nil { + return errors.New("failed to pack core document: %v", err) + } + + idKeys, ok := ctx.Self().Keys[identity.KeyPurposeSigning] + if !ok { + return errors.New("missing keys for signing") + } + + psv := PreSignatureRequestValidator(ctx.Self().ID[:], idKeys.PrivateKey, idKeys.PublicKey) + err = psv.Validate(nil, model) + if err != nil { + return errors.New("failed to validate model for signature request: %v", err) + } + + err = dp.p2pClient.GetSignaturesForDocument(ctx, dp.identityService, cd) + if err != nil { + return errors.New("failed to collect signatures from the collaborators: %v", err) + } + + err = model.UnpackCoreDocument(cd) + if err != nil { + return errors.New("failed to unpack core document: %v", err) + } + + return nil +} + +// PrepareForAnchoring validates the signatures and generates the document root +func (dp defaultProcessor) PrepareForAnchoring(model documents.Model) error { + cd, err := model.PackCoreDocument() + if err != nil { + return errors.New("failed to pack core document: %v", err) + } + + psv := PostSignatureRequestValidator(dp.identityService) + err = psv.Validate(nil, model) + if err != nil { + return errors.New("failed to validate signatures: %v", err) + } + + err = CalculateDocumentRoot(cd) + if err != nil { + return errors.New("failed to generate document root: %v", err) + } + + err = model.UnpackCoreDocument(cd) + if err != nil { + return errors.New("failed to unpack core document: %v", err) + } + + return nil +} + +// AnchorDocument validates the model, and anchors the document +func (dp defaultProcessor) AnchorDocument(ctx *header.ContextHeader, model documents.Model) error { + cd, err := model.PackCoreDocument() + if err != nil { + return errors.New("failed to pack core document: %v", err) + } + + pav := PreAnchorValidator(dp.identityService) + err = pav.Validate(nil, model) + if err != nil { + return errors.New("pre anchor validation failed: %v", err) + } + + rootHash, err := anchors.ToDocumentRoot(cd.DocumentRoot) + if err != nil { + return errors.New("failed to get document root: %v", err) + } + + id, err := dp.config.GetIdentityID() + if err != nil { + return errors.New("failed to get self cent ID: %v", err) + } + + centID, err := identity.ToCentID(id) + if err != nil { + return errors.New("centID invalid: %v", err) + } + + anchorID, err := anchors.ToAnchorID(cd.CurrentVersion) + if err != nil { + return errors.New("failed to get anchor ID: %v", err) + } + + // generate message authentication code for the anchor call + mac, err := secp256k1.SignEthereum(anchors.GenerateCommitHash(anchorID, centID, rootHash), ctx.Self().Keys[identity.KeyPurposeEthMsgAuth].PrivateKey) + if err != nil { + return errors.New("failed to generate ethereum MAC: %v", err) + } + + log.Infof("Anchoring document with identifiers: [document: %#x, current: %#x, next: %#x], rootHash: %#x", cd.DocumentIdentifier, cd.CurrentVersion, cd.NextVersion, cd.DocumentRoot) + confirmations, err := dp.anchorRepository.CommitAnchor(anchorID, rootHash, centID, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, mac) + if err != nil { + return errors.New("failed to commit anchor: %v", err) + } + + <-confirmations + log.Infof("Anchored document with identifiers: [document: %#x, current: %#x, next: %#x], rootHash: %#x", cd.DocumentIdentifier, cd.CurrentVersion, cd.NextVersion, cd.DocumentRoot) + return nil +} + +// SendDocument does post anchor validations and sends the document to collaborators +func (dp defaultProcessor) SendDocument(ctx *header.ContextHeader, model documents.Model) error { + cd, err := model.PackCoreDocument() + if err != nil { + return errors.New("failed to pack core document: %v", err) + } + + av := PostAnchoredValidator(dp.identityService, dp.anchorRepository) + err = av.Validate(nil, model) + if err != nil { + return errors.New("post anchor validations failed: %v", err) + } + + extCollaborators, err := GetExternalCollaborators(ctx.Self().ID, cd) + if err != nil { + return errors.New("get external collaborators failed: %v", err) + } + + for _, c := range extCollaborators { + cID, erri := identity.ToCentID(c) + if erri != nil { + err = errors.AppendError(err, erri) + continue + } + + erri = dp.Send(ctx, cd, cID) + if erri != nil { + err = errors.AppendError(err, erri) + } + } + + return err +} diff --git a/coredocument/processor/processor.go b/coredocument/processor/processor.go deleted file mode 100644 index 2fb58bba1..000000000 --- a/coredocument/processor/processor.go +++ /dev/null @@ -1,275 +0,0 @@ -package coredocumentprocessor - -import ( - "context" - "fmt" - - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" - "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/centerrors" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" - "github.com/centrifuge/go-centrifuge/keytools/secp256k1" - "github.com/centrifuge/go-centrifuge/p2p" - "github.com/centrifuge/go-centrifuge/signatures" - "github.com/centrifuge/go-centrifuge/utils" - "github.com/centrifuge/go-centrifuge/version" - logging "github.com/ipfs/go-log" -) - -var log = logging.Logger("coredocument") - -// Processor identifies an implementation, which can do a bunch of things with a CoreDocument. -// E.g. send, anchor, etc. -type Processor interface { - Send(ctx context.Context, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) - PrepareForSignatureRequests(model documents.Model) error - RequestSignatures(ctx context.Context, model documents.Model) error - PrepareForAnchoring(model documents.Model) error - AnchorDocument(model documents.Model) error - SendDocument(ctx context.Context, model documents.Model) error -} - -// defaultProcessor implements Processor interface -type defaultProcessor struct { - IdentityService identity.Service - P2PClient p2p.Client - AnchorRepository anchors.AnchorRepository -} - -// DefaultProcessor returns the default implementation of CoreDocument Processor -func DefaultProcessor(idService identity.Service, p2pClient p2p.Client, repository anchors.AnchorRepository) Processor { - return defaultProcessor{ - IdentityService: idService, - P2PClient: p2pClient, - AnchorRepository: repository, - } -} - -// Send sends the given defaultProcessor to the given recipient on the P2P layer -func (dp defaultProcessor) Send(ctx context.Context, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) { - if coreDocument == nil { - return centerrors.NilError(coreDocument) - } - - log.Infof("sending coredocument %x to recipient %x", coreDocument.DocumentIdentifier, recipient) - - id, err := dp.IdentityService.LookupIdentityForID(recipient) - if err != nil { - err = centerrors.Wrap(err, "error fetching receiver identity") - log.Error(err) - return err - } - - lastB58Key, err := id.GetCurrentP2PKey() - if err != nil { - err = centerrors.Wrap(err, "error fetching p2p key") - log.Error(err) - return err - } - - log.Infof("Sending Document to CentID [%v] with Key [%v]\n", recipient, lastB58Key) - clientWithProtocol := fmt.Sprintf("/ipfs/%s", lastB58Key) - client, err := dp.P2PClient.OpenClient(clientWithProtocol) - if err != nil { - return fmt.Errorf("failed to open client: %v", err) - } - - log.Infof("Done opening connection against [%s]\n", lastB58Key) - - idConfig, err := ed25519.GetIDConfig() - if err != nil { - err = centerrors.Wrap(err, "failed to extract bytes") - log.Error(err) - return err - } - - header := &p2ppb.CentrifugeHeader{ - SenderCentrifugeId: idConfig.ID, - CentNodeVersion: version.GetVersion().String(), - NetworkIdentifier: config.Config.GetNetworkID(), - } - _, err = client.SendAnchoredDocument(context.Background(), &p2ppb.AnchorDocumentRequest{Document: coreDocument, Header: header}) - if err != nil { - err = centerrors.Wrap(err, "failed to post to the node") - log.Error(err) - return err - } - - return nil -} - -// PrepareForSignatureRequests gets the core document from the model, and adds the node's own signature -func (dp defaultProcessor) PrepareForSignatureRequests(model documents.Model) error { - cd, err := model.PackCoreDocument() - if err != nil { - return fmt.Errorf("failed to pack core document: %v", err) - } - - // calculate the signing root - err = coredocument.CalculateSigningRoot(cd) - if err != nil { - return fmt.Errorf("failed to calculate signing root: %v", err) - } - - // sign document with own key and append it to signatures - idConfig, err := ed25519.GetIDConfig() - if err != nil { - return fmt.Errorf("failed to get keys for signing: %v", err) - } - sig := signatures.Sign(idConfig, cd.SigningRoot) - cd.Signatures = append(cd.Signatures, sig) - - err = model.UnpackCoreDocument(cd) - if err != nil { - return fmt.Errorf("failed to unpack the core document: %v", err) - } - - return nil -} - -// RequestSignatures gets the core document from the model, validates pre signature requirements, -// collects signatures, and validates the signatures, -func (dp defaultProcessor) RequestSignatures(ctx context.Context, model documents.Model) error { - cd, err := model.PackCoreDocument() - if err != nil { - return fmt.Errorf("failed to pack core document: %v", err) - } - - psv := coredocument.PreSignatureRequestValidator() - err = psv.Validate(nil, model) - if err != nil { - return fmt.Errorf("failed to validate model for signature request: %v", err) - } - - err = dp.P2PClient.GetSignaturesForDocument(ctx, cd) - if err != nil { - return fmt.Errorf("failed to collect signatures from the collaborators: %v", err) - } - - err = model.UnpackCoreDocument(cd) - if err != nil { - return fmt.Errorf("failed to unpack core document: %v", err) - } - - return nil -} - -// PrepareForAnchoring validates the signatures and generates the document root -func (dp defaultProcessor) PrepareForAnchoring(model documents.Model) error { - cd, err := model.PackCoreDocument() - if err != nil { - return fmt.Errorf("failed to pack core document: %v", err) - } - - psv := coredocument.PostSignatureRequestValidator() - err = psv.Validate(nil, model) - if err != nil { - return fmt.Errorf("failed to validate signatures: %v", err) - } - - err = coredocument.CalculateDocumentRoot(cd) - if err != nil { - return fmt.Errorf("failed to generate document root: %v", err) - } - - err = model.UnpackCoreDocument(cd) - if err != nil { - return fmt.Errorf("failed to unpack core document: %v", err) - } - - return nil -} - -// AnchorDocument validates the model, and anchors the document -func (dp defaultProcessor) AnchorDocument(model documents.Model) error { - cd, err := model.PackCoreDocument() - if err != nil { - return fmt.Errorf("failed to pack core document: %v", err) - } - - pav := coredocument.PreAnchorValidator() - err = pav.Validate(nil, model) - if err != nil { - return fmt.Errorf("pre anchor validation failed: %v", err) - } - - rootHash, err := anchors.NewDocRoot(cd.DocumentRoot) - if err != nil { - return fmt.Errorf("failed to get document root: %v", err) - } - - id, err := config.Config.GetIdentityID() - if err != nil { - return fmt.Errorf("failed to get self cent ID: %v", err) - } - - centID, err := identity.ToCentID(id) - if err != nil { - return fmt.Errorf("centID invalid: %v", err) - } - - // generate message authentication code for the anchor call - secpIDConfig, err := secp256k1.GetIDConfig() - if err != nil { - return fmt.Errorf("failed to get eth keys: %v", err) - } - - anchorID, err := anchors.NewAnchorID(cd.CurrentVersion) - if err != nil { - return fmt.Errorf("failed to get anchor ID: %v", err) - } - - mac, err := secp256k1.SignEthereum(anchors.GenerateCommitHash(anchorID, centID, rootHash), secpIDConfig.PrivateKey) - if err != nil { - return fmt.Errorf("failed to generate ethereum MAC: %v", err) - } - - log.Infof("Anchoring document with identifiers: [document: %#x, current: %#x, next: %#x], rootHash: %#x", cd.DocumentIdentifier, cd.CurrentVersion, cd.NextVersion, cd.DocumentRoot) - confirmations, err := dp.AnchorRepository.CommitAnchor(anchorID, rootHash, centID, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, mac) - if err != nil { - return fmt.Errorf("failed to commit anchor: %v", err) - } - - <-confirmations - log.Infof("Anchored document with identifiers: [document: %#x, current: %#x, next: %#x], rootHash: %#x", cd.DocumentIdentifier, cd.CurrentVersion, cd.NextVersion, cd.DocumentRoot) - return nil -} - -// SendDocument does post anchor validations and sends the document to collaborators -func (dp defaultProcessor) SendDocument(ctx context.Context, model documents.Model) error { - cd, err := model.PackCoreDocument() - if err != nil { - return fmt.Errorf("failed to pack core document: %v", err) - } - - av := coredocument.PostAnchoredValidator(dp.AnchorRepository) - err = av.Validate(nil, model) - if err != nil { - return fmt.Errorf("post anchor validations failed: %v", err) - } - - extCollaborators, err := coredocument.GetExternalCollaborators(cd) - if err != nil { - return fmt.Errorf("get external collaborators failed: %v", err) - } - - for _, c := range extCollaborators { - cID, erri := identity.ToCentID(c) - if erri != nil { - err = documents.AppendError(err, erri) - continue - } - - erri = dp.Send(ctx, cd, cID) - if erri != nil { - err = documents.AppendError(err, erri) - } - } - - return err -} diff --git a/coredocument/processor/processor_test.go b/coredocument/processor_test.go similarity index 56% rename from coredocument/processor/processor_test.go rename to coredocument/processor_test.go index 764c4ed66..b4826fa89 100644 --- a/coredocument/processor/processor_test.go +++ b/coredocument/processor_test.go @@ -1,24 +1,17 @@ // +build unit -package coredocumentprocessor +package coredocument import ( "context" - "fmt" - "math/big" - "os" "testing" "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/config" - "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" - cented25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" - "github.com/centrifuge/go-centrifuge/p2p" - "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/utils" "github.com/golang/protobuf/ptypes/any" @@ -27,19 +20,8 @@ import ( "golang.org/x/crypto/ed25519" ) -var dp defaultProcessor - -func TestMain(m *testing.M) { - dp = defaultProcessor{} - ibootstappers := []bootstrap.TestBootstrapper{ - &config.Bootstrapper{}, - } - bootstrap.RunTestBootstrappers(ibootstappers, nil) - result := m.Run() - os.Exit(result) -} - func TestCoreDocumentProcessor_SendNilDocument(t *testing.T) { + dp := DefaultProcessor(nil, nil, nil, cfg) err := dp.Send(nil, nil, [identity.CentIDLength]byte{}) assert.Error(t, err, "should have thrown an error") } @@ -61,10 +43,14 @@ func (m mockModel) UnpackCoreDocument(cd *coredocumentpb.CoreDocument) error { } func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { + srv := &testingcommons.MockIDService{} + dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) // pack failed model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err := dp.PrepareForSignatureRequests(model) + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) + err = dp.PrepareForSignatureRequests(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to pack core document") @@ -73,28 +59,28 @@ func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { model = mockModel{} // failed to get id - pub, _ := config.Config.GetSigningKeyPair() - config.Config.V.Set("keys.signing.publicKey", "wrong path") - cd = coredocument.New() + pub, _ := cfg.GetSigningKeyPair() + cfg.Set("keys.signing.publicKey", "wrong path") + cd = New() cd.DataRoot = utils.RandomSlice(32) cd.EmbeddedData = &any.Any{ TypeUrl: "some type", Value: []byte("some data"), } - assert.Nil(t, coredocument.FillSalts(cd)) + assert.Nil(t, FillSalts(cd)) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() - err = dp.PrepareForSignatureRequests(model) - model.AssertExpectations(t) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get keys for signing") - config.Config.V.Set("keys.signing.publicKey", pub) + ctxh, err = header.NewContextHeader(context.Background(), cfg) + assert.NotNil(t, err) + cfg.Set("keys.signing.publicKey", pub) + ctxh, err = header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) // failed unpack model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() - model.On("UnpackCoreDocument", cd).Return(fmt.Errorf("error")).Once() - err = dp.PrepareForSignatureRequests(model) + model.On("UnpackCoreDocument", cd).Return(errors.New("error")).Once() + err = dp.PrepareForSignatureRequests(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to unpack the core document") @@ -104,33 +90,36 @@ func TestDefaultProcessor_PrepareForSignatureRequests(t *testing.T) { model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() model.On("UnpackCoreDocument", cd).Return(nil).Once() - err = dp.PrepareForSignatureRequests(model) + err = dp.PrepareForSignatureRequests(ctxh, model) model.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, cd.Signatures) assert.Len(t, cd.Signatures, 1) sig := cd.Signatures[0] - id, err := cented25519.GetIDConfig() - assert.Nil(t, err) - assert.True(t, ed25519.Verify(id.PublicKey, cd.SigningRoot, sig.Signature)) + id := ctxh.Self() + assert.True(t, ed25519.Verify(id.Keys[identity.KeyPurposeSigning].PublicKey, cd.SigningRoot, sig.Signature)) } type p2pClient struct { mock.Mock - p2p.Client + client } -func (p p2pClient) GetSignaturesForDocument(ctx context.Context, doc *coredocumentpb.CoreDocument) error { +func (p p2pClient) GetSignaturesForDocument(ctx *header.ContextHeader, identityService identity.Service, doc *coredocumentpb.CoreDocument) error { args := p.Called(ctx, doc) return args.Error(0) } func TestDefaultProcessor_RequestSignatures(t *testing.T) { - // pack failed + srv := &testingcommons.MockIDService{} + dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) + // pack failed model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err := dp.RequestSignatures(ctx, model) + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to pack core document") @@ -139,31 +128,31 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { cd := new(coredocumentpb.CoreDocument) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) - err = dp.RequestSignatures(ctx, model) + err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to validate model for signature request") // failed signature collection - cd = coredocument.New() + cd = New() cd.DataRoot = utils.RandomSlice(32) cd.EmbeddedData = &any.Any{ TypeUrl: "some type", Value: []byte("some data"), } - assert.Nil(t, coredocument.FillSalts(cd)) + assert.Nil(t, FillSalts(cd)) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() model.On("UnpackCoreDocument", cd).Return(nil).Once() - err = dp.PrepareForSignatureRequests(model) + err = dp.PrepareForSignatureRequests(ctxh, model) assert.Nil(t, err) model.AssertExpectations(t) c := p2pClient{} - c.On("GetSignaturesForDocument", ctx, cd).Return(fmt.Errorf("error")).Once() - dp.P2PClient = c + c.On("GetSignaturesForDocument", ctxh, cd).Return(errors.New("error")).Once() + dp.p2pClient = c model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) - err = dp.RequestSignatures(ctx, model) + err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) c.AssertExpectations(t) assert.Error(t, err) @@ -171,12 +160,12 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { // unpack fail c = p2pClient{} - c.On("GetSignaturesForDocument", ctx, cd).Return(nil).Once() - dp.P2PClient = c + c.On("GetSignaturesForDocument", ctxh, cd).Return(nil).Once() + dp.p2pClient = c model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) - model.On("UnpackCoreDocument", cd).Return(fmt.Errorf("error")).Once() - err = dp.RequestSignatures(ctx, model) + model.On("UnpackCoreDocument", cd).Return(errors.New("error")).Once() + err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) c.AssertExpectations(t) assert.Error(t, err) @@ -184,21 +173,23 @@ func TestDefaultProcessor_RequestSignatures(t *testing.T) { // success c = p2pClient{} - c.On("GetSignaturesForDocument", ctx, cd).Return(nil).Once() - dp.P2PClient = c + c.On("GetSignaturesForDocument", ctxh, cd).Return(nil).Once() + dp.p2pClient = c model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) model.On("UnpackCoreDocument", cd).Return(nil).Once() - err = dp.RequestSignatures(ctx, model) + err = dp.RequestSignatures(ctxh, model) model.AssertExpectations(t) c.AssertExpectations(t) assert.Nil(t, err) } func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { + srv := &testingcommons.MockIDService{} + dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) // pack failed model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() err := dp.PrepareForAnchoring(model) model.AssertExpectations(t) assert.Error(t, err) @@ -214,40 +205,28 @@ func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { assert.Contains(t, err.Error(), "failed to validate signatures") // failed unpack - cd = coredocument.New() + cd = New() cd.DataRoot = utils.RandomSlice(32) cd.EmbeddedData = &any.Any{ TypeUrl: "some type", Value: []byte("some data"), } - assert.Nil(t, coredocument.FillSalts(cd)) - err = coredocument.CalculateSigningRoot(cd) + assert.Nil(t, FillSalts(cd)) + err = CalculateSigningRoot(cd) assert.Nil(t, err) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) - model.On("UnpackCoreDocument", cd).Return(fmt.Errorf("error")).Once() - c, err := cented25519.GetIDConfig() + model.On("UnpackCoreDocument", cd).Return(errors.New("error")).Once() + c, err := identity.GetIdentityConfig(cfg) assert.Nil(t, err) - s := signatures.Sign(c, cd.SigningRoot) + s := identity.Sign(c, identity.KeyPurposeSigning, cd.SigningRoot) cd.Signatures = []*coredocumentpb.Signature{s} - pubkey, err := utils.SliceToByte32(c.PublicKey) - assert.Nil(t, err) - idkey := &identity.EthereumIdentityKey{ - Key: pubkey, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - centID, err := identity.ToCentID(c.ID) assert.Nil(t, err) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + dp = DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) err = dp.PrepareForAnchoring(model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to unpack core document") @@ -255,17 +234,9 @@ func TestDefaultProcessor_PrepareForAnchoring(t *testing.T) { model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(4) model.On("UnpackCoreDocument", cd).Return(nil).Once() - id = &testingcommons.MockID{} - srv = &testingcommons.MockIDService{} - centID, err = identity.ToCentID(c.ID) - assert.Nil(t, err) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv err = dp.PrepareForAnchoring(model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, cd.DocumentRoot) } @@ -275,23 +246,29 @@ type mockRepo struct { anchors.AnchorRepository } -func (m mockRepo) CommitAnchor(anchorID anchors.AnchorID, documentRoot anchors.DocRoot, centrifugeID identity.CentID, documentProofs [][32]byte, signature []byte) (<-chan *anchors.WatchCommit, error) { +func (m mockRepo) CommitAnchor(anchorID anchors.AnchorID, documentRoot anchors.DocumentRoot, centrifugeID identity.CentID, documentProofs [][32]byte, signature []byte) (<-chan *anchors.WatchCommit, error) { args := m.Called(anchorID, documentRoot, centrifugeID, documentProofs, signature) c, _ := args.Get(0).(chan *anchors.WatchCommit) return c, args.Error(1) } -func (m mockRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocRoot, error) { +func (m mockRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { args := m.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocRoot) + docRoot, _ := args.Get(0).(anchors.DocumentRoot) return docRoot, args.Error(1) } func TestDefaultProcessor_AnchorDocument(t *testing.T) { + srv := &testingcommons.MockIDService{} + dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) + ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) + // pack failed model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err := dp.AnchorDocument(model) + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to pack core document") @@ -300,94 +277,61 @@ func TestDefaultProcessor_AnchorDocument(t *testing.T) { model = mockModel{} cd := new(coredocumentpb.CoreDocument) model.On("PackCoreDocument").Return(cd, nil).Times(5) - err = dp.AnchorDocument(model) + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "pre anchor validation failed") // get ID failed - cd = coredocument.New() + cd = New() cd.DataRoot = utils.RandomSlice(32) cd.EmbeddedData = &any.Any{ TypeUrl: "some type", Value: []byte("some data"), } - assert.Nil(t, coredocument.FillSalts(cd)) - assert.Nil(t, coredocument.CalculateSigningRoot(cd)) + assert.Nil(t, FillSalts(cd)) + assert.Nil(t, CalculateSigningRoot(cd)) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(5) - c, err := cented25519.GetIDConfig() + c, err := identity.GetIdentityConfig(cfg) assert.Nil(t, err) - s := signatures.Sign(c, cd.SigningRoot) + s := identity.Sign(c, identity.KeyPurposeSigning, cd.SigningRoot) cd.Signatures = []*coredocumentpb.Signature{s} - assert.Nil(t, coredocument.CalculateDocumentRoot(cd)) - pubkey, err := utils.SliceToByte32(c.PublicKey) - assert.Nil(t, err) - idkey := &identity.EthereumIdentityKey{ - Key: pubkey, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - centID, err := identity.ToCentID(c.ID) + assert.Nil(t, CalculateDocumentRoot(cd)) assert.Nil(t, err) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv - oldID := config.Config.V.GetString("identityId") - config.Config.V.Set("identityId", "wrong id") - err = dp.AnchorDocument(model) + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() + oldID := cfg.GetString("identityId") + cfg.Set("identityId", "wrong id") + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get self cent ID") - config.Config.V.Set("identityId", "0x0102030405060708") + cfg.Set("identityId", "0x0102030405060708") // wrong ID model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(5) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv - err = dp.AnchorDocument(model) - model.AssertExpectations(t) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err) - assert.Contains(t, err.Error(), "centID invalid") - config.Config.V.Set("identityId", oldID) + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() - // missing eth keys - oldPth := config.Config.V.Get("keys.ethauth.publicKey") - config.Config.V.Set("keys.ethauth.publicKey", "wrong path") - model = mockModel{} - model.On("PackCoreDocument").Return(cd, nil).Times(5) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv - err = dp.AnchorDocument(model) + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get eth keys") - config.Config.V.Set("keys.ethauth.publicKey", oldPth) + assert.Contains(t, err.Error(), "centID invalid") + cfg.Set("identityId", oldID) // failed anchor commit model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(5) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() + repo := mockRepo{} - repo.On("CommitAnchor", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("error")).Once() - dp.AnchorRepository = repo - err = dp.AnchorDocument(model) + repo.On("CommitAnchor", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() + dp.anchorRepository = repo + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) repo.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to commit anchor") @@ -395,29 +339,31 @@ func TestDefaultProcessor_AnchorDocument(t *testing.T) { // success model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(5) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() + repo = mockRepo{} ch := make(chan *anchors.WatchCommit, 1) ch <- new(anchors.WatchCommit) repo.On("CommitAnchor", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(ch, nil).Once() - dp.AnchorRepository = repo - err = dp.AnchorDocument(model) + dp.anchorRepository = repo + err = dp.AnchorDocument(ctxh, model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) repo.AssertExpectations(t) assert.Nil(t, err) } func TestDefaultProcessor_SendDocument(t *testing.T) { + srv := &testingcommons.MockIDService{} + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + dp := DefaultProcessor(srv, nil, nil, cfg).(defaultProcessor) ctx := context.Background() - + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) // pack failed model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err := dp.SendDocument(ctx, model) + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to pack core document") @@ -426,51 +372,36 @@ func TestDefaultProcessor_SendDocument(t *testing.T) { model = mockModel{} cd := new(coredocumentpb.CoreDocument) model.On("PackCoreDocument").Return(cd, nil).Times(6) - err = dp.SendDocument(ctx, model) + err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "post anchor validations failed") // failed send - cd = coredocument.New() + cd = New() cd.DataRoot = utils.RandomSlice(32) cd.EmbeddedData = &any.Any{ TypeUrl: "some type", Value: []byte("some data"), } cd.Collaborators = [][]byte{[]byte("some id")} - assert.Nil(t, coredocument.FillSalts(cd)) - assert.Nil(t, coredocument.CalculateSigningRoot(cd)) + assert.Nil(t, FillSalts(cd)) + assert.Nil(t, CalculateSigningRoot(cd)) model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Times(6) - c, err := cented25519.GetIDConfig() + c, err := identity.GetIdentityConfig(cfg) assert.Nil(t, err) - s := signatures.Sign(c, cd.SigningRoot) + s := identity.Sign(c, identity.KeyPurposeSigning, cd.SigningRoot) cd.Signatures = []*coredocumentpb.Signature{s} - assert.Nil(t, coredocument.CalculateDocumentRoot(cd)) - pubkey, err := utils.SliceToByte32(c.PublicKey) - assert.Nil(t, err) - idkey := &identity.EthereumIdentityKey{ - Key: pubkey, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - centID, err := identity.ToCentID(c.ID) - assert.Nil(t, err) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv - docRoot, err := anchors.NewDocRoot(cd.DocumentRoot) + assert.Nil(t, CalculateDocumentRoot(cd)) + docRoot, err := anchors.ToDocumentRoot(cd.DocumentRoot) assert.Nil(t, err) repo := mockRepo{} repo.On("GetDocumentRootOf", mock.Anything).Return(docRoot, nil).Once() - dp.AnchorRepository = repo - err = dp.SendDocument(ctx, model) + dp.anchorRepository = repo + err = dp.SendDocument(ctxh, model) model.AssertExpectations(t) srv.AssertExpectations(t) - id.AssertExpectations(t) repo.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid length byte slice provided for centID") diff --git a/coredocument/validator.go b/coredocument/validator.go index e1e16967d..ff39ff592 100644 --- a/coredocument/validator.go +++ b/coredocument/validator.go @@ -7,7 +7,8 @@ import ( "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" @@ -17,17 +18,17 @@ import ( func UpdateVersionValidator() documents.Validator { return documents.ValidatorFunc(func(old, new documents.Model) error { if old == nil || new == nil { - return fmt.Errorf("need both the old and new model") + return errors.New("need both the old and new model") } oldCD, err := old.PackCoreDocument() if err != nil { - return fmt.Errorf("failed to fetch old core document: %v", err) + return errors.New("failed to fetch old core document: %v", err) } newCD, err := new.PackCoreDocument() if err != nil { - return fmt.Errorf("failed to fetch new core document: %v", err) + return errors.New("failed to fetch new core document: %v", err) } checks := []struct { @@ -61,17 +62,17 @@ func UpdateVersionValidator() documents.Validator { for _, c := range checks { if !utils.CheckMultiple32BytesFilled(c.a, c.b) { - err = documents.AppendError(err, documents.NewError(c.name, "missing identifiers")) + err = errors.AppendError(err, documents.NewError(c.name, "missing identifiers")) continue } if !utils.IsSameByteSlice(c.a, c.b) { - err = documents.AppendError(err, documents.NewError(c.name, "mismatched")) + err = errors.AppendError(err, documents.NewError(c.name, "mismatched")) } } if utils.IsEmptyByteSlice(newCD.NextVersion) { - err = documents.AppendError(err, documents.NewError("cd_next_version", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_next_version", centerrors.RequiredField)) } return err @@ -81,12 +82,12 @@ func UpdateVersionValidator() documents.Validator { // getCoreDocument takes an model and returns the core document of the model func getCoreDocument(model documents.Model) (*coredocumentpb.CoreDocument, error) { if model == nil { - return nil, fmt.Errorf("nil model") + return nil, errors.New("nil model") } cd, err := model.PackCoreDocument() if err != nil { - return nil, fmt.Errorf("failed to pack core document: %v", err) + return nil, errors.New("failed to pack core document: %v", err) } return cd, nil @@ -101,23 +102,23 @@ func baseValidator() documents.Validator { } if cd == nil { - return fmt.Errorf("nil document") + return errors.New("nil document") } if utils.IsEmptyByteSlice(cd.DocumentIdentifier) { - err = documents.AppendError(err, documents.NewError("cd_identifier", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_identifier", centerrors.RequiredField)) } if utils.IsEmptyByteSlice(cd.CurrentVersion) { - err = documents.AppendError(err, documents.NewError("cd_current_version", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_current_version", centerrors.RequiredField)) } if utils.IsEmptyByteSlice(cd.NextVersion) { - err = documents.AppendError(err, documents.NewError("cd_next_version", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_next_version", centerrors.RequiredField)) } if utils.IsEmptyByteSlice(cd.DataRoot) { - err = documents.AppendError(err, documents.NewError("cd_data_root", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_data_root", centerrors.RequiredField)) } // double check the identifiers @@ -126,7 +127,7 @@ func baseValidator() documents.Validator { // Problem (re-using an old identifier for NextVersion): CurrentVersion or DocumentIdentifier same as NextVersion if isSameBytes(cd.NextVersion, cd.DocumentIdentifier) || isSameBytes(cd.NextVersion, cd.CurrentVersion) { - err = documents.AppendError(err, documents.NewError("cd_overall", centerrors.IdentifierReUsed)) + err = errors.AppendError(err, documents.NewError("cd_overall", centerrors.IdentifierReUsed)) } // lets not do verbose check like earlier since these will be @@ -138,7 +139,7 @@ func baseValidator() documents.Validator { salts.NextVersion, salts.DocumentIdentifier, salts.PreviousRoot) { - err = documents.AppendError(err, documents.NewError("cd_salts", centerrors.RequiredField)) + err = errors.AppendError(err, documents.NewError("cd_salts", centerrors.RequiredField)) } return err @@ -155,16 +156,16 @@ func signingRootValidator() documents.Validator { } if utils.IsEmptyByteSlice(cd.SigningRoot) { - return fmt.Errorf("signing root missing") + return errors.New("signing root missing") } tree, err := GetDocumentSigningTree(cd) if err != nil { - return fmt.Errorf("failed to calculate signing root: %v", err) + return errors.New("failed to calculate signing root: %v", err) } if !utils.IsSameByteSlice(cd.SigningRoot, tree.RootHash()) { - return fmt.Errorf("signing root mismatch") + return errors.New("signing root mismatch") } return nil @@ -181,16 +182,16 @@ func documentRootValidator() documents.Validator { } if utils.IsEmptyByteSlice(cd.DocumentRoot) { - return fmt.Errorf("document root missing") + return errors.New("document root missing") } tree, err := GetDocumentRootTree(cd) if err != nil { - return fmt.Errorf("failed to calculate document root: %v", err) + return errors.New("failed to calculate document root: %v", err) } if !utils.IsSameByteSlice(cd.DocumentRoot, tree.RootHash()) { - return fmt.Errorf("document root mismatch") + return errors.New("document root mismatch") } return nil @@ -201,7 +202,7 @@ func documentRootValidator() documents.Validator { // re-calculates the signature and compares with existing one // assumes signing_root is already generated and verified // Note: this needs to used only before document is sent for signatures from the collaborators -func readyForSignaturesValidator() documents.Validator { +func readyForSignaturesValidator(centIDBytes, priv, pub []byte) documents.Validator { return documents.ValidatorFunc(func(_, model documents.Model) error { cd, err := getCoreDocument(model) if err != nil { @@ -209,26 +210,21 @@ func readyForSignaturesValidator() documents.Validator { } if len(cd.Signatures) != 1 { - return fmt.Errorf("expecting only one signature") + return errors.New("expecting only one signature") } - c, err := ed25519.GetIDConfig() - if err != nil { - return fmt.Errorf("failed to get keys for signature calculation: %v", err) - } - - s := signatures.Sign(c, cd.SigningRoot) + s := signatures.Sign(centIDBytes, priv, pub, cd.SigningRoot) sh := cd.Signatures[0] if !utils.IsSameByteSlice(s.EntityId, sh.EntityId) { - err = documents.AppendError(err, documents.NewError("cd_entity_id", "entity ID mismatch")) + err = errors.AppendError(err, documents.NewError("cd_entity_id", "entity ID mismatch")) } if !utils.IsSameByteSlice(s.PublicKey, sh.PublicKey) { - err = documents.AppendError(err, documents.NewError("cd_public_key", "public key mismatch")) + err = errors.AppendError(err, documents.NewError("cd_public_key", "public key mismatch")) } if !utils.IsSameByteSlice(s.Signature, sh.Signature) { - err = documents.AppendError(err, documents.NewError("cd_signature", "signature mismatch")) + err = errors.AppendError(err, documents.NewError("cd_signature", "signature mismatch")) } return err @@ -239,7 +235,7 @@ func readyForSignaturesValidator() documents.Validator { // assumes signing root is verified // Note: can be used when during the signature request on collaborator side and post signature collection on sender side // Note: this will break the current flow where we proceed to anchor even signatures verification fails -func signaturesValidator() documents.Validator { +func signaturesValidator(idService identity.Service) documents.Validator { return documents.ValidatorFunc(func(_, model documents.Model) error { cd, err := getCoreDocument(model) if err != nil { @@ -247,14 +243,16 @@ func signaturesValidator() documents.Validator { } if len(cd.Signatures) < 1 { - return fmt.Errorf("atleast one signature expected") + return errors.New("atleast one signature expected") } for _, sig := range cd.Signatures { - if errI := signatures.ValidateSignature(sig, cd.SigningRoot); errI != nil { - err = documents.AppendError( + if erri := idService.ValidateSignature(sig, cd.SigningRoot); erri != nil { + err = errors.AppendError( err, - documents.NewError(fmt.Sprintf("signature_%s", hexutil.Encode(sig.EntityId)), "signature verification failed")) + documents.NewError( + fmt.Sprintf("signature_%s", hexutil.Encode(sig.EntityId)), + fmt.Sprintf("signature verification failed: %v", erri))) } } @@ -268,26 +266,26 @@ func anchoredValidator(repo anchors.AnchorRepository) documents.Validator { return documents.ValidatorFunc(func(_, new documents.Model) error { cd, err := getCoreDocument(new) if err != nil { - return fmt.Errorf("failed to get core document: %v", err) + return errors.New("failed to get core document: %v", err) } - anchorID, err := anchors.NewAnchorID(cd.CurrentVersion) + anchorID, err := anchors.ToAnchorID(cd.CurrentVersion) if err != nil { - return fmt.Errorf("failed to get anchorID: %v", err) + return errors.New("failed to get anchorID: %v", err) } - docRoot, err := anchors.NewDocRoot(cd.DocumentRoot) + docRoot, err := anchors.ToDocumentRoot(cd.DocumentRoot) if err != nil { - return fmt.Errorf("failed to get document root: %v", err) + return errors.New("failed to get document root: %v", err) } gotRoot, err := repo.GetDocumentRootOf(anchorID) if err != nil { - return fmt.Errorf("failed to get document root from chain: %v", err) + return errors.New("failed to get document root from chain: %v", err) } if !utils.IsSameByteSlice(docRoot[:], gotRoot[:]) { - return fmt.Errorf("mismatched document roots") + return errors.New("mismatched document roots") } return nil @@ -299,8 +297,8 @@ func anchoredValidator(repo anchors.AnchorRepository) documents.Validator { // signing root validator // signatures validator // should be used when node receives a document requesting for signature -func SignatureRequestValidator() documents.ValidatorGroup { - return PostSignatureRequestValidator() +func SignatureRequestValidator(idService identity.Service) documents.ValidatorGroup { + return PostSignatureRequestValidator(idService) } // PreAnchorValidator is a validator group with following validators @@ -309,9 +307,9 @@ func SignatureRequestValidator() documents.ValidatorGroup { // document root validator // signatures validator // should be called before pre anchoring -func PreAnchorValidator() documents.ValidatorGroup { +func PreAnchorValidator(idService identity.Service) documents.ValidatorGroup { return documents.ValidatorGroup{ - PostSignatureRequestValidator(), + PostSignatureRequestValidator(idService), documentRootValidator(), } } @@ -320,9 +318,9 @@ func PreAnchorValidator() documents.ValidatorGroup { // PreAnchorValidator // anchoredValidator // should be called after anchoring the document/when received anchored document -func PostAnchoredValidator(repo anchors.AnchorRepository) documents.ValidatorGroup { +func PostAnchoredValidator(idService identity.Service, repo anchors.AnchorRepository) documents.ValidatorGroup { return documents.ValidatorGroup{ - PreAnchorValidator(), + PreAnchorValidator(idService), anchoredValidator(repo), } } @@ -332,11 +330,11 @@ func PostAnchoredValidator(repo anchors.AnchorRepository) documents.ValidatorGro // signingRootValidator // readyForSignaturesValidator // should be called after sender signing the document and before requesting the document -func PreSignatureRequestValidator() documents.ValidatorGroup { +func PreSignatureRequestValidator(centIDBytes, priv, pub []byte) documents.ValidatorGroup { return documents.ValidatorGroup{ baseValidator(), signingRootValidator(), - readyForSignaturesValidator(), + readyForSignaturesValidator(centIDBytes, priv, pub), } } @@ -345,10 +343,10 @@ func PreSignatureRequestValidator() documents.ValidatorGroup { // signingRootValidator // signaturesValidator // should be called after the signature collection/before preparing for anchoring -func PostSignatureRequestValidator() documents.ValidatorGroup { +func PostSignatureRequestValidator(idService identity.Service) documents.ValidatorGroup { return documents.ValidatorGroup{ baseValidator(), signingRootValidator(), - signaturesValidator(), + signaturesValidator(idService), } } diff --git a/coredocument/validator_test.go b/coredocument/validator_test.go index f7dae91cf..3b7a64e6f 100644 --- a/coredocument/validator_test.go +++ b/coredocument/validator_test.go @@ -3,21 +3,16 @@ package coredocument import ( - "flag" - "fmt" - "math/big" - "os" "testing" + "context" + "github.com/centrifuge/centrifuge-protobufs/documenttypes" "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/config" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" - "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/utils" "github.com/golang/protobuf/ptypes/any" @@ -25,30 +20,6 @@ import ( "github.com/stretchr/testify/mock" ) -func TestMain(m *testing.M) { - ibootstappers := []bootstrap.TestBootstrapper{ - &config.Bootstrapper{}, - } - bootstrap.RunTestBootstrappers(ibootstappers, nil) - flag.Parse() - config.Config.V.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") - config.Config.V.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") - result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) - os.Exit(result) -} - -type mockModel struct { - mock.Mock - documents.Model -} - -func (m mockModel) PackCoreDocument() (*coredocumentpb.CoreDocument, error) { - args := m.Called() - cd, _ := args.Get(0).(*coredocumentpb.CoreDocument) - return cd, args.Error(1) -} - func TestUpdateVersionValidator(t *testing.T) { uvv := UpdateVersionValidator() @@ -59,21 +30,21 @@ func TestUpdateVersionValidator(t *testing.T) { // old model pack core doc fail old := mockModel{} - new := mockModel{} - old.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err = uvv.Validate(old, new) + newM := mockModel{} + old.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + err = uvv.Validate(old, newM) old.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to fetch old core document") - // new model pack core doc fail + // newM model pack core doc fail oldCD := New() oldCD.DocumentRoot = utils.RandomSlice(32) old.On("PackCoreDocument").Return(oldCD, nil).Once() - new.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() - err = uvv.Validate(old, new) + newM.On("PackCoreDocument").Return(nil, errors.New("error")).Once() + err = uvv.Validate(old, newM) old.AssertExpectations(t) - new.AssertExpectations(t) + newM.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to fetch new core document") @@ -81,21 +52,21 @@ func TestUpdateVersionValidator(t *testing.T) { newCD := New() newCD.NextVersion = nil old.On("PackCoreDocument").Return(oldCD, nil).Once() - new.On("PackCoreDocument").Return(newCD, nil).Once() - err = uvv.Validate(old, new) + newM.On("PackCoreDocument").Return(newCD, nil).Once() + err = uvv.Validate(old, newM) old.AssertExpectations(t) - new.AssertExpectations(t) + newM.AssertExpectations(t) assert.Error(t, err) - assert.Len(t, documents.ConvertToMap(err), 4) + assert.Equal(t, 5, errors.Len(err)) // success newCD, err = PrepareNewVersion(*oldCD, nil) assert.Nil(t, err) old.On("PackCoreDocument").Return(oldCD, nil).Once() - new.On("PackCoreDocument").Return(newCD, nil).Once() - err = uvv.Validate(old, new) + newM.On("PackCoreDocument").Return(newCD, nil).Once() + err = uvv.Validate(old, newM) old.AssertExpectations(t) - new.AssertExpectations(t) + newM.AssertExpectations(t) assert.Nil(t, err) } @@ -107,7 +78,7 @@ func Test_getCoreDocument(t *testing.T) { // pack core document fail model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() cd, err = getCoreDocument(model) model.AssertExpectations(t) assert.Error(t, err) @@ -128,7 +99,7 @@ func TestValidator_baseValidator(t *testing.T) { // fail getCoreDocument model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() err := bv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -139,7 +110,7 @@ func TestValidator_baseValidator(t *testing.T) { model.On("PackCoreDocument").Return(cd, nil).Once() err = bv.Validate(nil, model) assert.Error(t, err) - assert.Contains(t, err.Error(), "cd_salts : Required field") + assert.Equal(t, "cd_salts : Required field", errors.GetErrs(err)[1].Error()) // success model = mockModel{} @@ -155,7 +126,7 @@ func TestValidator_signingRootValidator(t *testing.T) { // fail getCoreDoc model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() err := sv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -199,7 +170,7 @@ func TestValidator_documentRootValidator(t *testing.T) { // fail getCoreDoc model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() err := dv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -235,12 +206,15 @@ func TestValidator_documentRootValidator(t *testing.T) { } func TestValidator_selfSignatureValidator(t *testing.T) { - rfsv := readyForSignaturesValidator() + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) + idKeys := ctxh.Self().Keys[identity.KeyPurposeSigning] + rfsv := readyForSignaturesValidator(ctxh.Self().ID[:], idKeys.PrivateKey, idKeys.PublicKey) // fail getCoreDoc model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() - err := rfsv.Validate(nil, model) + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() + err = rfsv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -267,13 +241,13 @@ func TestValidator_selfSignatureValidator(t *testing.T) { err = rfsv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) - assert.Len(t, documents.ConvertToMap(err), 3) + assert.Equal(t, 3, errors.Len(err)) // success cd.SigningRoot = utils.RandomSlice(32) - c, err := ed25519.GetIDConfig() + c, err := identity.GetIdentityConfig(cfg) assert.Nil(t, err) - s = signatures.Sign(c, cd.SigningRoot) + s = identity.Sign(c, identity.KeyPurposeSigning, cd.SigningRoot) cd.Signatures = []*coredocumentpb.Signature{s} model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() @@ -283,11 +257,12 @@ func TestValidator_selfSignatureValidator(t *testing.T) { } func TestValidator_signatureValidator(t *testing.T) { - ssv := signaturesValidator() + srv := &testingcommons.MockIDService{} + ssv := signaturesValidator(srv) // fail getCoreDoc model := mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("err")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("err")).Once() err := ssv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -305,6 +280,7 @@ func TestValidator_signatureValidator(t *testing.T) { // failed validation model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(errors.New("fail")).Once() s := &coredocumentpb.Signature{EntityId: utils.RandomSlice(7)} cd.Signatures = append(cd.Signatures, s) err = ssv.Validate(nil, model) @@ -315,34 +291,18 @@ func TestValidator_signatureValidator(t *testing.T) { // success model = mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() + srv.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil).Once() cd.SigningRoot = utils.RandomSlice(32) - c, err := ed25519.GetIDConfig() - assert.Nil(t, err) - s = signatures.Sign(c, cd.SigningRoot) - cd.Signatures = []*coredocumentpb.Signature{s} - pubkey, err := utils.SliceToByte32(c.PublicKey) - assert.Nil(t, err) - idkey := &identity.EthereumIdentityKey{ - Key: pubkey, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - centID, err := identity.ToCentID(c.ID) - assert.Nil(t, err) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubkey[:]).Return(idkey, nil).Once() - identity.IDService = srv + cd.Signatures = []*coredocumentpb.Signature{{}} + err = ssv.Validate(nil, model) model.AssertExpectations(t) - id.AssertExpectations(t) srv.AssertExpectations(t) assert.Nil(t, err) } func TestPreAnchorValidator(t *testing.T) { - pav := PreAnchorValidator() + pav := PreAnchorValidator(nil) assert.Len(t, pav, 2) } @@ -351,9 +311,9 @@ type repo struct { anchors.AnchorRepository } -func (r repo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocRoot, error) { +func (r repo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocRoot) + docRoot, _ := args.Get(0).(anchors.DocumentRoot) return docRoot, args.Error(1) } @@ -384,12 +344,12 @@ func TestValidator_anchoredValidator(t *testing.T) { assert.Contains(t, err.Error(), "failed to get document root") // failed to get docRoot from chain - anchorID, err := anchors.NewAnchorID(utils.RandomSlice(32)) + anchorID, err := anchors.ToAnchorID(utils.RandomSlice(32)) assert.Nil(t, err) r := &repo{} av = anchoredValidator(r) cd.CurrentVersion = anchorID[:] - r.On("GetDocumentRootOf", anchorID).Return(nil, fmt.Errorf("error")).Once() + r.On("GetDocumentRootOf", anchorID).Return(nil, errors.New("error")).Once() cd.DocumentRoot = utils.RandomSlice(32) model = &mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() @@ -400,7 +360,7 @@ func TestValidator_anchoredValidator(t *testing.T) { assert.Contains(t, err.Error(), "failed to get document root from chain") // mismatched doc roots - docRoot := anchors.NewRandomDocRoot() + docRoot := anchors.RandomDocumentRoot() r = &repo{} av = anchoredValidator(r) r.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() @@ -440,7 +400,7 @@ func TestValidate_baseValidator(t *testing.T) { NextVersion: id4, DataRoot: id5, }, - key: "cd_salts", + key: "[cd_salts : Required field]", }, // salts missing previous root @@ -458,7 +418,7 @@ func TestValidate_baseValidator(t *testing.T) { DataRoot: id4, }, }, - key: "cd_salts", + key: "[cd_salts : Required field]", }, // missing identifiers in core document @@ -476,7 +436,7 @@ func TestValidate_baseValidator(t *testing.T) { PreviousRoot: id5, }, }, - key: "cd_data_root", + key: "[cd_data_root : Required field]", }, // missing identifiers in core document and salts @@ -493,7 +453,7 @@ func TestValidate_baseValidator(t *testing.T) { DataRoot: id4, }, }, - key: "cd_data_root", + key: "[cd_data_root : Required field; cd_salts : Required field]", }, // repeated identifiers @@ -512,7 +472,7 @@ func TestValidate_baseValidator(t *testing.T) { PreviousRoot: id5, }, }, - key: "cd_overall", + key: "[cd_overall : Identifier re-used]", }, // repeated identifiers @@ -531,7 +491,7 @@ func TestValidate_baseValidator(t *testing.T) { PreviousRoot: id5, }, }, - key: "cd_overall", + key: "[cd_overall : Identifier re-used]", }, // All okay @@ -566,27 +526,30 @@ func TestValidate_baseValidator(t *testing.T) { continue } - assert.Contains(t, err.Error(), c.key) + assert.Equal(t, c.key, err.Error()) } } func TestPostAnchoredValidator(t *testing.T) { - pav := PostAnchoredValidator(nil) + pav := PostAnchoredValidator(nil, nil) assert.Len(t, pav, 2) } func TestPreSignatureRequestValidator(t *testing.T) { - psv := PreSignatureRequestValidator() + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) + idKeys := ctxh.Self().Keys[identity.KeyPurposeSigning] + psv := PreSignatureRequestValidator(ctxh.Self().ID[:], idKeys.PrivateKey, idKeys.PublicKey) assert.Len(t, psv, 3) } func TestPostSignatureRequestValidator(t *testing.T) { - psv := PostSignatureRequestValidator() + psv := PostSignatureRequestValidator(nil) assert.Len(t, psv, 3) } func TestSignatureRequestValidator(t *testing.T) { - srv := SignatureRequestValidator() + srv := SignatureRequestValidator(nil) assert.Len(t, srv, 3) } diff --git a/documents/anchor.go b/documents/anchor.go index 15a368b9a..558c468d5 100644 --- a/documents/anchor.go +++ b/documents/anchor.go @@ -1,19 +1,19 @@ package documents import ( - "context" - "fmt" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" ) // anchorProcessor has same methods to coredoc processor // this is to avoid import cycles // this will disappear once we have queueing logic in place type anchorProcessor interface { - PrepareForSignatureRequests(model Model) error - RequestSignatures(ctx context.Context, model Model) error + PrepareForSignatureRequests(ctx *header.ContextHeader, model Model) error + RequestSignatures(ctx *header.ContextHeader, model Model) error PrepareForAnchoring(model Model) error - AnchorDocument(model Model) error - SendDocument(ctx context.Context, model Model) error + AnchorDocument(ctx *header.ContextHeader, model Model) error + SendDocument(ctx *header.ContextHeader, model Model) error } // updaterFunc is a wrapper that will be called to save the state of the model between processor steps @@ -21,16 +21,16 @@ type updaterFunc func(id []byte, model Model) error // AnchorDocument add signature, requests signatures, anchors document, and sends the anchored document // to collaborators -func AnchorDocument(ctx context.Context, model Model, proc anchorProcessor, updater updaterFunc) (Model, error) { +func AnchorDocument(ctx *header.ContextHeader, model Model, proc anchorProcessor, updater updaterFunc) (Model, error) { cd, err := model.PackCoreDocument() if err != nil { return nil, err } id := cd.CurrentVersion - err = proc.PrepareForSignatureRequests(model) + err = proc.PrepareForSignatureRequests(ctx, model) if err != nil { - return nil, fmt.Errorf("failed to prepare document for signatures: %v", err) + return nil, errors.NewTypedError(ErrDocumentAnchoring, errors.New("failed to prepare document for signatures: %v", err)) } err = updater(id, model) @@ -40,17 +40,17 @@ func AnchorDocument(ctx context.Context, model Model, proc anchorProcessor, upda err = proc.RequestSignatures(ctx, model) if err != nil { - return nil, fmt.Errorf("failed to collect signatures: %v", err) + return nil, errors.NewTypedError(ErrDocumentAnchoring, errors.New("failed to collect signatures: %v", err)) } err = updater(id, model) if err != nil { - return nil, err + return nil, errors.NewTypedError(ErrDocumentAnchoring, err) } err = proc.PrepareForAnchoring(model) if err != nil { - return nil, fmt.Errorf("failed to prepare for anchoring: %v", err) + return nil, errors.NewTypedError(ErrDocumentAnchoring, errors.New("failed to prepare for anchoring: %v", err)) } err = updater(id, model) @@ -58,24 +58,24 @@ func AnchorDocument(ctx context.Context, model Model, proc anchorProcessor, upda return nil, err } - err = proc.AnchorDocument(model) + err = proc.AnchorDocument(ctx, model) if err != nil { - return nil, fmt.Errorf("failed to anchor document: %v", err) + return nil, errors.NewTypedError(ErrDocumentAnchoring, errors.New("failed to anchor document: %v", err)) } err = updater(id, model) if err != nil { - return nil, err + return nil, errors.NewTypedError(ErrDocumentAnchoring, err) } err = proc.SendDocument(ctx, model) if err != nil { - return nil, fmt.Errorf("failed to send anchored document: %v", err) + return nil, errors.NewTypedError(ErrDocumentAnchoring, errors.New("failed to send anchored document: %v", err)) } err = updater(id, model) if err != nil { - return nil, err + return nil, errors.NewTypedError(ErrDocumentAnchoring, err) } return model, nil diff --git a/documents/bootstrapper.go b/documents/bootstrapper.go index a636b16b3..aa7f4208b 100644 --- a/documents/bootstrapper.go +++ b/documents/bootstrapper.go @@ -1,8 +1,28 @@ package documents +import ( + "github.com/centrifuge/go-centrifuge/storage" + "github.com/syndtr/goleveldb/leveldb" +) + +const ( + // BootstrappedRegistry is the key to ServiceRegistry in Bootstrap context + BootstrappedRegistry = "BootstrappedRegistry" + // BootstrappedDocumentRepository is the key to the database repository of documents + BootstrappedDocumentRepository = "BootstrappedDocumentRepository" +) + +// Bootstrapper implements bootstrap.Bootstrapper. type Bootstrapper struct{} // Bootstrap sets the required storage and registers -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { +func (Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + ctx[BootstrappedRegistry] = NewServiceRegistry() + ldb, ok := ctx[storage.BootstrappedLevelDB].(*leveldb.DB) + if !ok { + return ErrDocumentBootstrap + } + repo := NewLevelDBRepository(ldb) + ctx[BootstrappedDocumentRepository] = repo return nil } diff --git a/documents/bootstrapper_test.go b/documents/bootstrapper_test.go new file mode 100644 index 000000000..dea6e5383 --- /dev/null +++ b/documents/bootstrapper_test.go @@ -0,0 +1,21 @@ +// +build unit + +package documents + +import ( + "testing" + + "github.com/centrifuge/go-centrifuge/storage" + "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb" +) + +func TestBootstrapper_Bootstrap(t *testing.T) { + ctx := make(map[string]interface{}) + ctx[storage.BootstrappedLevelDB] = &leveldb.DB{} + err := Bootstrapper{}.Bootstrap(ctx) + assert.Nil(t, err) + assert.NotNil(t, ctx[BootstrappedRegistry]) + _, ok := ctx[BootstrappedRegistry].(*ServiceRegistry) + assert.True(t, ok) +} diff --git a/documents/error.go b/documents/error.go index 098220468..40cd52fef 100644 --- a/documents/error.go +++ b/documents/error.go @@ -1,122 +1,97 @@ package documents import ( - "bytes" "fmt" - "github.com/hashicorp/go-multierror" + "github.com/centrifuge/go-centrifuge/errors" ) -// Error wraps an error with specific key -type Error struct { - key string - err error -} - -// Error returns the underlying error message -func (e Error) Error() string { - return e.err.Error() -} +const ( -// New creates a new error from a key and a msg -func NewError(key, msg string) error { - err := fmt.Errorf(msg) - return Error{key: key, err: err} -} + // ErrDocumentConfigTenantID must be used for errors related to tenantID operations + ErrDocumentConfigTenantID = errors.Error("error with tenantID operations") -// Append function is used to create a list of errors. -// First argument can be nil, a multierror.Error, or any other error -func AppendError(dstErr, srcErr error) error { - _, ok := dstErr.(*multierror.Error) - result := multierror.Append(dstErr, srcErr) - - // if dstErr is not a multierror.Error newly created multierror.Error result needs formatting - if !ok { - return format(result) - } - return result -} + // ErrDocumentBootstrap must be used for errors related to documents package bootstrapping + ErrDocumentBootstrap = errors.Error("error when bootstrapping document package") -func format(err *multierror.Error) error { - err.ErrorFormat = func(errorList []error) string { - var buffer bytes.Buffer - for i, err := range errorList { - if errt, ok := err.(Error); ok { - buffer.WriteString(fmt.Sprintf("%s : %s\n", errt.key, errt.err.Error())) - continue - } + // ErrDocumentIdentifier must be used for errors caused by document identifier problems + ErrDocumentIdentifier = errors.Error("document identifier error") - buffer.WriteString(fmt.Sprintf("Error %v : %s\n", i+1, err.Error())) - } + // ErrDocumentInvalidType must be used when a provided document type is not valid to be processed by the service + ErrDocumentInvalidType = errors.Error("document is of invalid type") - buffer.WriteString(fmt.Sprintf("Total Errors: %v\n", len(errorList))) - return buffer.String() - } + // ErrDocumentNil must be used when the provided document through a function is nil + ErrDocumentNil = errors.Error("no(nil) document provided") - return err -} + // 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") -// Errors returns an array of errors -func Errors(err error) []error { - if err == nil { - return nil - } + // ErrDocumentNotFound must be used to indicate that the document for provided id is not found in the system + ErrDocumentNotFound = errors.Error("document not found in the system database") - if multiErr, ok := err.(*multierror.Error); ok { - return multiErr.Errors - } + // ErrDocumentVersionNotFound must be used to indicate that the specified version of the document for provided id is not found in the system + ErrDocumentVersionNotFound = errors.Error("specified version of the document not found in the system database") - return []error{err} -} + // ErrDocumentPersistence must be used when creating or updating a document in the system database failed + ErrDocumentPersistence = errors.Error("error encountered when storing document in the system database") -// Len returns the amount of embedded errors -func LenError(err error) int { + // ErrDocumentPackingCoreDocument must be used when packing of core document for the given document failed + ErrDocumentPackingCoreDocument = errors.Error("core document packing failed") - if err == nil { - return 0 - } + // ErrDocumentUnPackingCoreDocument must be used when unpacking of core document for the given document failed + ErrDocumentUnPackingCoreDocument = errors.Error("core document unpacking failed") - if multiErr, ok := err.(*multierror.Error); ok { + // ErrDocumentPrepareCoreDocument must be used when preparing a new core document fails for the given document + ErrDocumentPrepareCoreDocument = errors.Error("core document preparation failed") - return multiErr.Len() - } - return 1 + // ErrDocumentSigning must be used when document signing related functionality fails + ErrDocumentSigning = errors.Error("document signing failed") -} + // ErrDocumentAnchoring must be used when document anchoring fails + ErrDocumentAnchoring = errors.Error("document anchoring failed") -func addToMap(errorMap map[string]string, key, msg string) map[string]string { - if errorMap[key] != "" { - errorMap[key] = fmt.Sprintf("%s\n%s", errorMap[key], msg) + // ErrDocumentCollaborator must be used when there is an error in processing collaborators + ErrDocumentCollaborator = errors.Error("document collaborator issue") - } else { - errorMap[key] = msg + // ErrDocumentProof must be used when document proof creation fails + ErrDocumentProof = errors.Error("document proof error") - } - return errorMap -} + // Document repository errors -// ConvertToMap converts errors into a map. -func ConvertToMap(err error) map[string]string { + // ErrDocumentRepositoryModelNotRegistered must be used when the model hasn't been registered in the database repository + ErrDocumentRepositoryModelNotRegistered = errors.Error("document model hasn't been registered in the database repository") - errorMap := make(map[string]string) - var key string - var standardErrorCounter int + // ErrDocumentRepositorySerialisation must be used when document repository encounters a marshalling error + ErrDocumentRepositorySerialisation = errors.Error("document repository encountered a marshalling error") - errors := Errors(err) + // ErrDocumentRepositoryModelNotFound must be used when document repository can not locate the given model + ErrDocumentRepositoryModelNotFound = errors.Error("document repository could not locate the given model") - for _, err := range errors { - if err, ok := err.(Error); ok { - key = err.key + // ErrDocumentRepositoryModelSave must be used when document repository can not save the given model + ErrDocumentRepositoryModelSave = errors.Error("document repository could not save the given model") - } else { - standardErrorCounter++ - key = fmt.Sprintf("error_%v", standardErrorCounter) + // ErrDocumentRepositoryModelAllReadyExists must be used when document repository finds an already existing model when saving + ErrDocumentRepositoryModelAllReadyExists = errors.Error("document repository found an already existing model when saving") - } + // ErrDocumentRepositoryModelDoesntExist must be used when document repository does not find an existing model for an update + ErrDocumentRepositoryModelDoesntExist = errors.Error("document repository did not find an existing model for an update") +) - addToMap(errorMap, key, err.Error()) +// Error wraps an error with specific key +// Deprecated: in favour of Error type in `github.com/centrifuge/go-centrifuge/errors` +type Error struct { + key string + err error +} - } - return errorMap +// Error returns the underlying error message +func (e Error) Error() string { + return fmt.Sprintf("%s : %s", e.key, e.err) +} +// NewError creates a new error from a key and a msg. +// Deprecated: in favour of Error type in `github.com/centrifuge/go-centrifuge/errors` +func NewError(key, msg string) error { + err := errors.New(msg) + return Error{key: key, err: err} } diff --git a/documents/error_test.go b/documents/error_test.go deleted file mode 100644 index 7f0d4393a..000000000 --- a/documents/error_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// +build unit - -package documents - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewError(t *testing.T) { - - err := NewError("test_error", "error msg") - assert.Error(t, err, "New should return an error") -} - -func TestAppendError(t *testing.T) { - - err := fmt.Errorf("test error") - - err = AppendError(nil, err) - assert.Equal(t, 1, LenError(err), "err should only include one error") - - err = AppendError(err, fmt.Errorf("second error")) - assert.Equal(t, 2, LenError(err), "err should include two errors") - assert.Equal(t, 2, len(Errors(err)), "returned error array should include two errors") - - err = AppendError(fmt.Errorf("err 1"), fmt.Errorf("err 2")) - assert.Equal(t, 2, LenError(err), "err should include two errors") - - err = AppendError(err, NewError("test", "error")) - assert.Len(t, Errors(err), 3) - assert.Contains(t, err.Error(), "test : error") -} - -func TestLenError(t *testing.T) { - - err := fmt.Errorf("test error") - - assert.Equal(t, 0, LenError(nil), "nil should return len 0") - assert.Equal(t, 1, LenError(err), "normal error should have length 1") - -} - -func TestErrors(t *testing.T) { - - err := AppendError(fmt.Errorf("err 1"), fmt.Errorf("err 2")) - errArray := Errors(err) - assert.Equal(t, 2, len(errArray), "array should contain two errors") - - errArray = Errors(fmt.Errorf("err 1")) - assert.Equal(t, 1, len(errArray), "array should contain one errors") - - errArray = Errors(nil) - assert.Equal(t, 0, len(errArray), "array should be nil") - -} - -func TestConvertToMap(t *testing.T) { - - err := NewError("test1", "error msg") - - err = AppendError(err, NewError("test2", "error msg2")) - - errMap := ConvertToMap(err) - assert.Equal(t, 2, len(errMap), "map should have two entries") - - err = AppendError(err, fmt.Errorf("standard error")) - err = AppendError(err, fmt.Errorf("standard error2")) - errMap = ConvertToMap(err) - assert.Equal(t, 4, len(errMap), "map should have 4 entries") - - assert.NotEqual(t, "", errMap["error_1"], "first standard error should have id 'error_1'") - assert.Equal(t, "", errMap["error_3"], "no standard error with id 'error_3'") - - errMap = ConvertToMap(nil) - assert.Equal(t, 0, len(errMap), "map should be empty") - - fmt.Print(errMap) - -} diff --git a/documents/handler.go b/documents/handler.go index 3d203c61b..3d7519122 100644 --- a/documents/handler.go +++ b/documents/handler.go @@ -15,20 +15,21 @@ var apiLog = logging.Logger("document-api") // grpcHandler handles all the common document related actions: proof generation type grpcHandler struct { + registry *ServiceRegistry } // GRPCHandler returns an implementation of documentpb.DocumentServiceServer -func GRPCHandler() documentpb.DocumentServiceServer { - return grpcHandler{} +func GRPCHandler(registry *ServiceRegistry) documentpb.DocumentServiceServer { + return grpcHandler{registry: registry} } // CreateDocumentProof creates precise proofs for the given fields -func (grpcHandler) CreateDocumentProof(ctx context.Context, createDocumentProofEnvelope *documentpb.CreateDocumentProofRequest) (*documentpb.DocumentProof, error) { +func (h grpcHandler) CreateDocumentProof(ctx context.Context, createDocumentProofEnvelope *documentpb.CreateDocumentProofRequest) (*documentpb.DocumentProof, error) { apiLog.Infof("Document proof request %v", createDocumentProofEnvelope) - service, err := GetRegistryInstance().LocateService(createDocumentProofEnvelope.Type) + service, err := h.registry.LocateService(createDocumentProofEnvelope.Type) if err != nil { - return &documentpb.DocumentProof{}, err + return &documentpb.DocumentProof{}, centerrors.Wrap(err, "could not locate service for document type") } identifier, err := hexutil.Decode(createDocumentProofEnvelope.Identifier) @@ -44,12 +45,12 @@ func (grpcHandler) CreateDocumentProof(ctx context.Context, createDocumentProofE } // CreateDocumentProofForVersion creates precise proofs for the given fields for the given version of the document -func (grpcHandler) CreateDocumentProofForVersion(ctx context.Context, createDocumentProofForVersionEnvelope *documentpb.CreateDocumentProofForVersionRequest) (*documentpb.DocumentProof, error) { +func (h grpcHandler) CreateDocumentProofForVersion(ctx context.Context, createDocumentProofForVersionEnvelope *documentpb.CreateDocumentProofForVersionRequest) (*documentpb.DocumentProof, error) { apiLog.Infof("Document proof request %v", createDocumentProofForVersionEnvelope) - service, err := GetRegistryInstance().LocateService(createDocumentProofForVersionEnvelope.Type) + service, err := h.registry.LocateService(createDocumentProofForVersionEnvelope.Type) if err != nil { - return &documentpb.DocumentProof{}, err + return &documentpb.DocumentProof{}, centerrors.Wrap(err, "could not locate service for document type") } identifier, err := hexutil.Decode(createDocumentProofForVersionEnvelope.Identifier) @@ -73,8 +74,8 @@ func (grpcHandler) CreateDocumentProofForVersion(ctx context.Context, createDocu func ConvertDocProofToClientFormat(proof *DocumentProof) (*documentpb.DocumentProof, error) { return &documentpb.DocumentProof{ Header: &documentpb.ResponseHeader{ - DocumentId: hexutil.Encode(proof.DocumentId), - VersionId: hexutil.Encode(proof.VersionId), + DocumentId: hexutil.Encode(proof.DocumentID), + VersionId: hexutil.Encode(proof.VersionID), State: proof.State, }, FieldProofs: ConvertProofsToClientFormat(proof.FieldProofs)}, nil @@ -92,7 +93,7 @@ func ConvertProofsToClientFormat(proofs []*proofspb.Proof) []*documentpb.Proof { // ConvertProofToClientFormat converts a proof in precise proof format in to a client protobuf proof func ConvertProofToClientFormat(proof *proofspb.Proof) *documentpb.Proof { return &documentpb.Proof{ - Property: proof.Property, + Property: proof.GetReadableName(), Value: proof.Value, Salt: hexutil.Encode(proof.Salt), Hash: hexutil.Encode(proof.Hash), diff --git a/documents/handler_test.go b/documents/handler_test.go index 69d57d198..1c346cbca 100644 --- a/documents/handler_test.go +++ b/documents/handler_test.go @@ -10,13 +10,14 @@ import ( "github.com/centrifuge/go-centrifuge/protobufs/gen/go/documents" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" + "github.com/centrifuge/precise-proofs/proofs" "github.com/centrifuge/precise-proofs/proofs/proto" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" ) func TestGrpcHandler_CreateDocumentProof(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProof" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -28,7 +29,7 @@ func TestGrpcHandler_CreateDocumentProof(t *testing.T) { id, _ := hexutil.Decode(req.Identifier) doc := &documents.DocumentProof{} service.On("CreateProofs", id, req.Fields).Return(doc, nil) - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) retDoc, _ := grpcHandler.CreateDocumentProof(context.TODO(), req) service.AssertExpectations(t) conv, _ := documents.ConvertDocProofToClientFormat(doc) @@ -36,7 +37,7 @@ func TestGrpcHandler_CreateDocumentProof(t *testing.T) { } func TestGrpcHandler_CreateDocumentProofUnableToLocateService(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofUnableToLocateService" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -45,14 +46,14 @@ func TestGrpcHandler_CreateDocumentProofUnableToLocateService(t *testing.T) { Type: "wrongService", Fields: []string{"field1"}, } - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) _, err := grpcHandler.CreateDocumentProof(context.TODO(), req) assert.NotNil(t, err) service.AssertNotCalled(t, "CreateProofs") } func TestGrpcHandler_CreateDocumentProofInvalidHex(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofInvalidHex" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -61,14 +62,14 @@ func TestGrpcHandler_CreateDocumentProofInvalidHex(t *testing.T) { Type: serviceName, Fields: []string{"field1"}, } - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) _, err := grpcHandler.CreateDocumentProof(context.TODO(), req) assert.NotNil(t, err) service.AssertNotCalled(t, "CreateProofs") } func TestGrpcHandler_CreateDocumentProofForVersion(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofForVersion" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -80,9 +81,9 @@ func TestGrpcHandler_CreateDocumentProofForVersion(t *testing.T) { } id, _ := hexutil.Decode(req.Identifier) version, _ := hexutil.Decode(req.Version) - doc := &documents.DocumentProof{DocumentId: utils.RandomSlice(32)} + doc := &documents.DocumentProof{DocumentID: utils.RandomSlice(32)} service.On("CreateProofsForVersion", id, version, req.Fields).Return(doc, nil) - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) retDoc, _ := grpcHandler.CreateDocumentProofForVersion(context.TODO(), req) service.AssertExpectations(t) conv, _ := documents.ConvertDocProofToClientFormat(doc) @@ -90,7 +91,7 @@ func TestGrpcHandler_CreateDocumentProofForVersion(t *testing.T) { } func TestGrpcHandler_CreateDocumentProofForVersionUnableToLocateService(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofForVersionUnableToLocateService" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -100,14 +101,14 @@ func TestGrpcHandler_CreateDocumentProofForVersionUnableToLocateService(t *testi Type: "wrongService", Fields: []string{"field1"}, } - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) _, err := grpcHandler.CreateDocumentProofForVersion(context.TODO(), req) assert.NotNil(t, err) service.AssertNotCalled(t, "CreateProofsForVersion") } func TestGrpcHandler_CreateDocumentProofForVersionInvalidHexForId(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofForVersionInvalidHexForId" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -117,14 +118,14 @@ func TestGrpcHandler_CreateDocumentProofForVersionInvalidHexForId(t *testing.T) Type: serviceName, Fields: []string{"field1"}, } - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) _, err := grpcHandler.CreateDocumentProofForVersion(context.TODO(), req) assert.NotNil(t, err) service.AssertNotCalled(t, "CreateProofsForVersion") } func TestGrpcHandler_CreateDocumentProofForVersionInvalidHexForVersion(t *testing.T) { - registry := documents.GetRegistryInstance() + registry := documents.NewServiceRegistry() serviceName := "CreateDocumentProofForVersionInvalidHexForVersion" service := &testingdocuments.MockService{} registry.Register(serviceName, service) @@ -134,7 +135,7 @@ func TestGrpcHandler_CreateDocumentProofForVersionInvalidHexForVersion(t *testin Type: serviceName, Fields: []string{"field1"}, } - grpcHandler := documents.GRPCHandler() + grpcHandler := documents.GRPCHandler(registry) _, err := grpcHandler.CreateDocumentProofForVersion(context.TODO(), req) assert.NotNil(t, err) service.AssertNotCalled(t, "CreateProofsForVersion") @@ -149,12 +150,12 @@ func TestConvertDocProofToClientFormat(t *testing.T) { { name: "happy", input: &documents.DocumentProof{ - DocumentId: []byte{1, 2, 1}, - VersionId: []byte{1, 2, 2}, + DocumentID: []byte{1, 2, 1}, + VersionID: []byte{1, 2, 2}, State: "state", FieldProofs: []*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "val1", Salt: []byte{1, 2, 3}, Hash: []byte{1, 2, 4}, @@ -212,7 +213,7 @@ func TestConvertDocProofToClientFormat(t *testing.T) { func TestConvertProofsToClientFormat(t *testing.T) { clientFormat := documents.ConvertProofsToClientFormat([]*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "val1", Salt: utils.RandomSlice(32), Hash: utils.RandomSlice(32), @@ -223,7 +224,7 @@ func TestConvertProofsToClientFormat(t *testing.T) { }, }, { - Property: "prop2", + Property: proofs.ReadableName("prop2"), Value: "val2", Salt: utils.RandomSlice(32), Hash: utils.RandomSlice(32), diff --git a/documents/invoice/bootstrapper.go b/documents/invoice/bootstrapper.go index 395682a46..30191c79c 100644 --- a/documents/invoice/bootstrapper.go +++ b/documents/invoice/bootstrapper.go @@ -1,31 +1,60 @@ package invoice import ( - "errors" - "fmt" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/coredocument/processor" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/p2p" ) +// Bootstrapper implements bootstrap.Bootstrapper. type Bootstrapper struct{} // Bootstrap sets the required storage and registers -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - if _, ok := context[bootstrap.BootstrappedLevelDb]; !ok { - return errors.New("initializing LevelDB repository failed") +func (Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + if _, ok := ctx[bootstrap.BootstrappedConfig]; !ok { + return errors.New("config hasn't been initialized") } + cfg := ctx[bootstrap.BootstrappedConfig].(config.Configuration) + + p2pClient, ok := ctx[p2p.BootstrappedP2PClient].(p2p.Client) + if !ok { + return errors.New("p2p client not initialised") + } + + registry, ok := ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("service registry not initialised") + } + + anchorRepo, ok := ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) + if !ok { + return errors.New("anchor repository not initialised") + } + + idService, ok := ctx[identity.BootstrappedIDService].(identity.Service) + if !ok { + return errors.New("identity service not initialised") + } + + repo, ok := ctx[documents.BootstrappedDocumentRepository].(documents.Repository) + if !ok { + return errors.New("document db repository not initialised") + } + repo.Register(&Invoice{}) + // register service - srv := DefaultService(getRepository(), coredocumentprocessor.DefaultProcessor(identity.IDService, p2p.NewP2PClient(), anchors.GetAnchorRepository()), anchors.GetAnchorRepository()) - err := documents.GetRegistryInstance().Register(documenttypes.InvoiceDataTypeUrl, srv) + srv := DefaultService(cfg, repo, coredocument.DefaultProcessor(idService, p2pClient, anchorRepo, cfg), anchorRepo, idService) + err := registry.Register(documenttypes.InvoiceDataTypeUrl, srv) if err != nil { - return fmt.Errorf("failed to register invoice service: %v", err) + return errors.New("failed to register invoice service: %v", err) } return nil diff --git a/documents/invoice/bootstrapper_test.go b/documents/invoice/bootstrapper_test.go index 211b23a36..e299b8ead 100644 --- a/documents/invoice/bootstrapper_test.go +++ b/documents/invoice/bootstrapper_test.go @@ -3,33 +3,9 @@ package invoice import ( - "flag" - "os" "testing" - - "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/context/testlogging" - "github.com/centrifuge/go-centrifuge/storage" ) -func TestMain(m *testing.M) { - ibootstappers := []bootstrap.TestBootstrapper{ - &testlogging.TestLoggingBootstrapper{}, - &config.Bootstrapper{}, - &storage.Bootstrapper{}, - &anchors.Bootstrapper{}, - &Bootstrapper{}, - } - - bootstrap.RunTestBootstrappers(ibootstappers, nil) - flag.Parse() - result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) - os.Exit(result) -} - func TestBootstrapper_Bootstrap(t *testing.T) { //err := (&Bootstrapper{}).Bootstrap(map[string]interface{}{}) //assert.Error(t, err, "Should throw an error because of empty context") diff --git a/documents/invoice/handler.go b/documents/invoice/handler.go index 9ea0db8ab..0cf9a091e 100644 --- a/documents/invoice/handler.go +++ b/documents/invoice/handler.go @@ -6,11 +6,14 @@ import ( "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/code" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/documents" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" "github.com/ethereum/go-ethereum/common/hexutil" logging "github.com/ipfs/go-log" "golang.org/x/net/context" + + "github.com/centrifuge/go-centrifuge/header" ) var apiLog = logging.Logger("invoice-api") @@ -19,22 +22,26 @@ var apiLog = logging.Logger("invoice-api") // anchoring, sending, finding stored invoice document type grpcHandler struct { service Service + config config.Configuration } // GRPCHandler returns an implementation of invoice.DocumentServiceServer -func GRPCHandler() (clientinvoicepb.DocumentServiceServer, error) { - invoiceService, err := documents.GetRegistryInstance().LocateService(documenttypes.InvoiceDataTypeUrl) +func GRPCHandler(config config.Configuration, registry *documents.ServiceRegistry) (clientinvoicepb.DocumentServiceServer, error) { + srv, err := registry.LocateService(documenttypes.InvoiceDataTypeUrl) if err != nil { return nil, err } + return &grpcHandler{ - service: invoiceService.(Service), + service: srv.(Service), + config: config, }, nil } // Create handles the creation of the invoices and anchoring the documents on chain func (h *grpcHandler) Create(ctx context.Context, req *clientinvoicepb.InvoiceCreatePayload) (*clientinvoicepb.InvoiceResponse, error) { - ctxHeader, err := documents.NewContextHeader() + apiLog.Debugf("Create request %v", req) + ctxHeader, err := header.NewContextHeader(ctx, h.config) if err != nil { apiLog.Error(err) return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get header: %v", err)) @@ -43,22 +50,29 @@ func (h *grpcHandler) Create(ctx context.Context, req *clientinvoicepb.InvoiceCr doc, err := h.service.DeriveFromCreatePayload(req, ctxHeader) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive create payload") } // validate and persist - doc, err = h.service.Create(ctx, doc) + doc, err = h.service.Create(ctxHeader, doc) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not create document") } - return h.service.DeriveInvoiceResponse(doc) + resp, err := h.service.DeriveInvoiceResponse(doc) + if err != nil { + apiLog.Error(err) + return nil, centerrors.Wrap(err, "could not derive response") + } + + return resp, nil } // Update handles the document update and anchoring func (h *grpcHandler) Update(ctx context.Context, payload *clientinvoicepb.InvoiceUpdatePayload) (*clientinvoicepb.InvoiceResponse, error) { - ctxHeader, err := documents.NewContextHeader() + apiLog.Debugf("Update request %v", payload) + ctxHeader, err := header.NewContextHeader(ctx, h.config) if err != nil { apiLog.Error(err) return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get header: %v", err)) @@ -67,59 +81,74 @@ func (h *grpcHandler) Update(ctx context.Context, payload *clientinvoicepb.Invoi doc, err := h.service.DeriveFromUpdatePayload(payload, ctxHeader) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive update payload") } - doc, err = h.service.Update(ctx, doc) + doc, err = h.service.Update(ctxHeader, doc) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not update document") } - return h.service.DeriveInvoiceResponse(doc) + resp, err := h.service.DeriveInvoiceResponse(doc) + if err != nil { + apiLog.Error(err) + return nil, centerrors.Wrap(err, "could not derive response") + } + + return resp, nil } // GetVersion returns the requested version of the document func (h *grpcHandler) GetVersion(ctx context.Context, getVersionRequest *clientinvoicepb.GetVersionRequest) (*clientinvoicepb.InvoiceResponse, error) { + apiLog.Debugf("Get version request %v", getVersionRequest) identifier, err := hexutil.Decode(getVersionRequest.Identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "identifier is invalid") } + version, err := hexutil.Decode(getVersionRequest.Version) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "version is invalid") } + model, err := h.service.GetVersion(identifier, version) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "document not found") } + resp, err := h.service.DeriveInvoiceResponse(model) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive response") } + return resp, nil } // Get returns the invoice the latest version of the document with given identifier func (h *grpcHandler) Get(ctx context.Context, getRequest *clientinvoicepb.GetRequest) (*clientinvoicepb.InvoiceResponse, error) { + apiLog.Debugf("Get request %v", getRequest) identifier, err := hexutil.Decode(getRequest.Identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "identifier is an invalid hex string") } + model, err := h.service.GetCurrentVersion(identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "document not found") } + resp, err := h.service.DeriveInvoiceResponse(model) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive response") } + return resp, nil } diff --git a/documents/invoice/handler_test.go b/documents/invoice/handler_test.go index 9d662cbd4..cdf518c1b 100644 --- a/documents/invoice/handler_test.go +++ b/documents/invoice/handler_test.go @@ -4,11 +4,12 @@ package invoice import ( "context" - "fmt" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" @@ -20,13 +21,13 @@ type mockService struct { mock.Mock } -func (m *mockService) DeriveFromCreatePayload(payload *clientinvoicepb.InvoiceCreatePayload, contextHeader *documents.ContextHeader) (documents.Model, error) { +func (m *mockService) DeriveFromCreatePayload(payload *clientinvoicepb.InvoiceCreatePayload, contextHeader *header.ContextHeader) (documents.Model, error) { args := m.Called(payload, contextHeader) model, _ := args.Get(0).(documents.Model) return model, args.Error(1) } -func (m *mockService) Create(ctx context.Context, inv documents.Model) (documents.Model, error) { +func (m *mockService) Create(ctx *header.ContextHeader, inv documents.Model) (documents.Model, error) { args := m.Called(ctx, inv) model, _ := args.Get(0).(documents.Model) return model, args.Error(1) @@ -56,27 +57,27 @@ func (m *mockService) DeriveInvoiceResponse(doc documents.Model) (*clientinvoice return data, args.Error(1) } -func (m *mockService) Update(ctx context.Context, doc documents.Model) (documents.Model, error) { +func (m *mockService) Update(ctx *header.ContextHeader, doc documents.Model) (documents.Model, error) { args := m.Called(ctx, doc) doc1, _ := args.Get(0).(documents.Model) return doc1, args.Error(1) } -func (m *mockService) DeriveFromUpdatePayload(payload *clientinvoicepb.InvoiceUpdatePayload, contextHeader *documents.ContextHeader) (documents.Model, error) { +func (m *mockService) DeriveFromUpdatePayload(payload *clientinvoicepb.InvoiceUpdatePayload, contextHeader *header.ContextHeader) (documents.Model, error) { args := m.Called(payload, contextHeader) doc, _ := args.Get(0).(documents.Model) return doc, args.Error(1) } func getHandler() *grpcHandler { - return &grpcHandler{service: &mockService{}} + return &grpcHandler{service: &mockService{}, config: cfg} } func TestGRPCHandler_Create_derive_fail(t *testing.T) { // DeriveFrom payload fails h := getHandler() srv := h.service.(*mockService) - srv.On("DeriveFromCreatePayload", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("derive failed")).Once() + srv.On("DeriveFromCreatePayload", mock.Anything, mock.Anything).Return(nil, errors.New("derive failed")).Once() _, err := h.Create(context.Background(), nil) srv.AssertExpectations(t) assert.Error(t, err, "must be non nil") @@ -87,7 +88,7 @@ func TestGRPCHandler_Create_create_fail(t *testing.T) { h := getHandler() srv := h.service.(*mockService) srv.On("DeriveFromCreatePayload", mock.Anything, mock.Anything).Return(new(Invoice), nil).Once() - srv.On("Create", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("create failed")).Once() + srv.On("Create", mock.Anything, mock.Anything).Return(nil, errors.New("create failed")).Once() payload := &clientinvoicepb.InvoiceCreatePayload{Data: &clientinvoicepb.InvoiceData{GrossAmount: 300}} _, err := h.Create(context.Background(), payload) srv.AssertExpectations(t) @@ -119,7 +120,7 @@ func TestGRPCHandler_Create_DeriveInvoiceResponse_fail(t *testing.T) { model := new(Invoice) srv.On("DeriveFromCreatePayload", mock.Anything, mock.Anything).Return(model, nil).Once() srv.On("Create", mock.Anything, mock.Anything).Return(model, nil).Once() - srv.On("DeriveInvoiceResponse", mock.Anything).Return(nil, fmt.Errorf("derive response failed")) + srv.On("DeriveInvoiceResponse", mock.Anything).Return(nil, errors.New("derive response failed")) payload := &clientinvoicepb.InvoiceCreatePayload{Data: &clientinvoicepb.InvoiceData{Currency: "EUR"}} _, err := h.Create(context.Background(), payload) srv.AssertExpectations(t) @@ -155,7 +156,7 @@ func TestGrpcHandler_Get_invalid_input(t *testing.T) { assert.EqualError(t, err, "identifier is an invalid hex string: hex string without 0x prefix") payload.Identifier = identifier - srv.On("GetCurrentVersion", identifierBytes).Return(nil, fmt.Errorf("not found")) + srv.On("GetCurrentVersion", identifierBytes).Return(nil, errors.New("not found")) res, err = h.Get(context.Background(), payload) srv.AssertExpectations(t) assert.Nil(t, res) @@ -194,7 +195,7 @@ func TestGrpcHandler_GetVersion_invalid_input(t *testing.T) { payload.Version = "0x00" payload.Identifier = "0x01" - mockErr := fmt.Errorf("not found") + mockErr := errors.New("not found") srv.On("GetVersion", []byte{0x01}, []byte{0x00}).Return(nil, mockErr) res, err = h.GetVersion(context.Background(), payload) srv.AssertExpectations(t) @@ -223,7 +224,7 @@ func TestGrpcHandler_Update_derive_fail(t *testing.T) { h := getHandler() srv := h.service.(*mockService) payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "0x010201"} - srv.On("DeriveFromUpdatePayload", payload, mock.Anything).Return(nil, fmt.Errorf("derive error")).Once() + srv.On("DeriveFromUpdatePayload", payload, mock.Anything).Return(nil, errors.New("derive error")).Once() res, err := h.Update(context.Background(), payload) srv.AssertExpectations(t) assert.Error(t, err) @@ -236,9 +237,11 @@ func TestGrpcHandler_Update_update_fail(t *testing.T) { srv := h.service.(*mockService) model := &mockModel{} ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "0x010201"} srv.On("DeriveFromUpdatePayload", payload, mock.Anything).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(nil, fmt.Errorf("update error")).Once() + srv.On("Update", ctxh, model).Return(nil, errors.New("update error")).Once() res, err := h.Update(ctx, payload) srv.AssertExpectations(t) assert.Error(t, err) @@ -251,10 +254,12 @@ func TestGrpcHandler_Update_derive_response_fail(t *testing.T) { srv := h.service.(*mockService) model := &mockModel{} ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "0x010201"} srv.On("DeriveFromUpdatePayload", payload, mock.Anything).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(model, nil).Once() - srv.On("DeriveInvoiceResponse", model).Return(nil, fmt.Errorf("derive response error")).Once() + srv.On("Update", ctxh, model).Return(model, nil).Once() + srv.On("DeriveInvoiceResponse", model).Return(nil, errors.New("derive response error")).Once() res, err := h.Update(ctx, payload) srv.AssertExpectations(t) assert.Error(t, err) @@ -267,10 +272,12 @@ func TestGrpcHandler_Update(t *testing.T) { srv := h.service.(*mockService) model := &mockModel{} ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "0x010201"} resp := &clientinvoicepb.InvoiceResponse{} srv.On("DeriveFromUpdatePayload", payload, mock.Anything).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(model, nil).Once() + srv.On("Update", ctxh, model).Return(model, nil).Once() srv.On("DeriveInvoiceResponse", model).Return(resp, nil).Once() res, err := h.Update(ctx, payload) srv.AssertExpectations(t) diff --git a/documents/invoice/model.go b/documents/invoice/model.go index 0b4b7b2f9..ad9b59f98 100644 --- a/documents/invoice/model.go +++ b/documents/invoice/model.go @@ -3,15 +3,14 @@ package invoice import ( "crypto/sha256" "encoding/json" - "fmt" "reflect" "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/centerrors" "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" "github.com/centrifuge/precise-proofs/proofs" @@ -22,40 +21,33 @@ import ( "github.com/golang/protobuf/ptypes/timestamp" ) +const prefix string = "invoice" + // Invoice implements the documents.Model keeps track of invoice related fields and state type Invoice struct { - // invoice number or reference number - InvoiceNumber string - // name of the sender company - SenderName string - // street and address details of the sender company - SenderStreet string - SenderCity string - SenderZipcode string - // country ISO code of the sender of this invoice - SenderCountry string - // name of the recipient company - RecipientName string + InvoiceNumber string // invoice number or reference number + SenderName string // name of the sender company + SenderStreet string // street and address details of the sender company + SenderCity string + SenderZipcode string // country ISO code of the sender of this invoice + SenderCountry string + RecipientName string // name of the recipient company RecipientStreet string RecipientCity string RecipientZipcode string - // country ISO code of the receipient of this invoice - RecipientCountry string - // ISO currency code - Currency string - // invoice amount including tax - GrossAmount int64 - // invoice amount excluding tax - NetAmount int64 - TaxAmount int64 - TaxRate int64 - Recipient *identity.CentID - Sender *identity.CentID - Payee *identity.CentID - Comment string - DueDate *timestamp.Timestamp - DateCreated *timestamp.Timestamp - ExtraData []byte + RecipientCountry string // country ISO code of the recipient of this invoice + Currency string // country ISO code of the recipient of this invoice + GrossAmount int64 // invoice amount including tax + NetAmount int64 // invoice amount excluding tax + TaxAmount int64 + TaxRate int64 + Recipient *identity.CentID + Sender *identity.CentID + Payee *identity.CentID + Comment string + DueDate *timestamp.Timestamp + DateCreated *timestamp.Timestamp + ExtraData []byte InvoiceSalts *invoicepb.InvoiceDataSalts CoreDocument *coredocumentpb.CoreDocument @@ -156,17 +148,17 @@ func (i *Invoice) createP2PProtobuf() *invoicepb.InvoiceData { } // InitInvoiceInput initialize the model based on the received parameters from the rest api call -func (i *Invoice) InitInvoiceInput(payload *clientinvoicepb.InvoiceCreatePayload, contextHeader *documents.ContextHeader) error { +func (i *Invoice) InitInvoiceInput(payload *clientinvoicepb.InvoiceCreatePayload, contextHeader *header.ContextHeader) error { err := i.initInvoiceFromData(payload.Data) if err != nil { return err } - collaborators := append([]string{contextHeader.Self().String()}, payload.Collaborators...) + collaborators := append([]string{contextHeader.Self().ID.String()}, payload.Collaborators...) i.CoreDocument, err = coredocument.NewWithCollaborators(collaborators) if err != nil { - return fmt.Errorf("failed to init core document: %v", err) + return errors.New("failed to init core document: %v", err) } return nil @@ -209,7 +201,7 @@ func (i *Invoice) initInvoiceFromData(data *clientinvoicepb.InvoiceData) error { if data.ExtraData != "" { ed, err := hexutil.Decode(data.ExtraData) if err != nil { - return centerrors.Wrap(err, "failed to decode extra data") + return errors.NewTypedError(err, errors.New("failed to decode extra data")) } i.ExtraData = ed @@ -266,6 +258,8 @@ func (i *Invoice) getInvoiceSalts(invoiceData *invoicepb.InvoiceData) *invoicepb return i.InvoiceSalts } +// ID returns document identifier. +// Note: this is not a unique identifier for each version of the document. func (i *Invoice) ID() ([]byte, error) { coreDoc, err := i.PackCoreDocument() if err != nil { @@ -280,7 +274,7 @@ func (i *Invoice) PackCoreDocument() (*coredocumentpb.CoreDocument, error) { invoiceData := i.createP2PProtobuf() serializedInvoice, err := proto.Marshal(invoiceData) if err != nil { - return nil, centerrors.Wrap(err, "couldn't serialise InvoiceData") + return nil, errors.NewTypedError(err, errors.New("couldn't serialise InvoiceData")) } invoiceAny := any.Any{ @@ -292,7 +286,7 @@ func (i *Invoice) PackCoreDocument() (*coredocumentpb.CoreDocument, error) { serializedSalts, err := proto.Marshal(invoiceSalt) if err != nil { - return nil, centerrors.Wrap(err, "couldn't serialise InvoiceSalts") + return nil, errors.NewTypedError(err, errors.New("couldn't serialise InvoiceSalts")) } invoiceSaltsAny := any.Any{ @@ -310,14 +304,14 @@ func (i *Invoice) PackCoreDocument() (*coredocumentpb.CoreDocument, error) { // UnpackCoreDocument unpacks the core document into Invoice func (i *Invoice) UnpackCoreDocument(coreDoc *coredocumentpb.CoreDocument) error { if coreDoc == nil { - return centerrors.NilError(coreDoc) + return errors.New("core document provided is nil %v", coreDoc) } if coreDoc.EmbeddedData == nil || coreDoc.EmbeddedData.TypeUrl != documenttypes.InvoiceDataTypeUrl || coreDoc.EmbeddedDataSalts == nil || coreDoc.EmbeddedDataSalts.TypeUrl != documenttypes.InvoiceSaltsTypeUrl { - return fmt.Errorf("trying to convert document with incorrect schema") + return errors.New("trying to convert document with incorrect schema") } invoiceData := &invoicepb.InvoiceData{} @@ -361,7 +355,7 @@ func (i *Invoice) Type() reflect.Type { func (i *Invoice) calculateDataRoot() error { t, err := i.getDocumentDataTree() if err != nil { - return fmt.Errorf("calculateDataRoot error %v", err) + return errors.New("calculateDataRoot error %v", err) } i.CoreDocument.DataRoot = t.RootHash() return nil @@ -369,15 +363,16 @@ func (i *Invoice) calculateDataRoot() error { // getDocumentDataTree creates precise-proofs data tree for the model func (i *Invoice) getDocumentDataTree() (tree *proofs.DocumentTree, err error) { - t := proofs.NewDocumentTree(proofs.TreeOptions{EnableHashSorting: true, Hash: sha256.New()}) + prop := proofs.NewProperty(prefix) + t := proofs.NewDocumentTree(proofs.TreeOptions{EnableHashSorting: true, Hash: sha256.New(), ParentPrefix: &prop}) invoiceData := i.createP2PProtobuf() err = t.AddLeavesFromDocument(invoiceData, i.getInvoiceSalts(invoiceData)) if err != nil { - return nil, fmt.Errorf("getDocumentDataTree error %v", err) + return nil, errors.New("getDocumentDataTree error %v", err) } err = t.Generate() if err != nil { - return nil, fmt.Errorf("getDocumentDataTree error %v", err) + return nil, errors.New("getDocumentDataTree error %v", err) } return &t, nil } @@ -388,14 +383,14 @@ func (i *Invoice) createProofs(fields []string) (coreDoc *coredocumentpb.CoreDoc // is still not saved with roots in db due to failures during getting signatures. coreDoc, err = i.PackCoreDocument() if err != nil { - return nil, nil, fmt.Errorf("createProofs error %v", err) + return nil, nil, errors.New("createProofs error %v", err) } tree, err := i.getDocumentDataTree() if err != nil { - return coreDoc, nil, fmt.Errorf("createProofs error %v", err) + return coreDoc, nil, errors.New("createProofs error %v", err) } proofs, err = coredocument.CreateProofs(tree, coreDoc, fields) - return + return coreDoc, proofs, err } diff --git a/documents/invoice/model_test.go b/documents/invoice/model_test.go index fe9b1b342..fe11037f6 100644 --- a/documents/invoice/model_test.go +++ b/documents/invoice/model_test.go @@ -3,16 +3,28 @@ package invoice import ( + "context" "encoding/json" + "os" "reflect" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/invoice" + "github.com/centrifuge/go-centrifuge/anchors" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/context/testlogging" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/p2p" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" + "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" @@ -20,6 +32,33 @@ import ( "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + +func TestMain(m *testing.M) { + ethClient := &testingcommons.MockEthClient{} + ethClient.On("GetEthClient").Return(nil) + ctx[ethereum.BootstrappedEthereumClient] = ethClient + + ibootstappers := []bootstrap.TestBootstrapper{ + &testlogging.TestLoggingBootstrapper{}, + &config.Bootstrapper{}, + &storage.Bootstrapper{}, + &queue.Bootstrapper{}, + &identity.Bootstrapper{}, + anchors.Bootstrapper{}, + documents.Bootstrapper{}, + p2p.Bootstrapper{}, + &Bootstrapper{}, + &queue.Starter{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} + func TestInvoice_FromCoreDocuments_invalidParameter(t *testing.T) { invoiceModel := &Invoice{} @@ -144,7 +183,7 @@ func TestInvoiceModel_getClientData(t *testing.T) { } func TestInvoiceModel_InitInvoiceInput(t *testing.T) { - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // fail recipient data := &clientinvoicepb.InvoiceData{ @@ -199,12 +238,12 @@ func TestInvoiceModel_InitInvoiceInput(t *testing.T) { assert.Equal(t, inv.Payee[:], []byte{1, 2, 3, 3, 4, 5}) assert.Equal(t, inv.Recipient[:], []byte{1, 2, 3, 4, 5, 6}) assert.Equal(t, inv.ExtraData[:], []byte{1, 2, 3, 2, 3, 1}) - id := contextHeader.Self() + id := contextHeader.Self().ID assert.Equal(t, inv.CoreDocument.Collaborators, [][]byte{id[:], {1, 1, 2, 4, 5, 6}, {1, 2, 3, 2, 3, 2}}) } func TestInvoiceModel_calculateDataRoot(t *testing.T) { - ctxHeader, err := documents.NewContextHeader() + ctxHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) m := new(Invoice) err = m.InitInvoiceInput(testingdocuments.CreateInvoicePayload(), ctxHeader) @@ -221,7 +260,7 @@ func TestInvoiceModel_calculateDataRoot(t *testing.T) { func TestInvoiceModel_createProofs(t *testing.T) { i, corDoc, err := createMockInvoice(t) assert.Nil(t, err) - corDoc, proof, err := i.createProofs([]string{"invoice_number", "collaborators[0]", "document_type"}) + corDoc, proof, err := i.createProofs([]string{"invoice.invoice_number", "collaborators[0]", "document_type"}) assert.Nil(t, err) assert.NotNil(t, proof) assert.NotNil(t, corDoc) @@ -237,6 +276,9 @@ func TestInvoiceModel_createProofs(t *testing.T) { assert.Nil(t, err) assert.True(t, valid) + // Validate '0x' Hex format in []byte value + assert.Equal(t, hexutil.Encode(i.CoreDocument.Collaborators[0]), proof[1].Value) + // Validate document_type valid, err = tree.ValidateProof(proof[2]) assert.Nil(t, err) @@ -261,8 +303,9 @@ func TestInvoiceModel_getDocumentDataTree(t *testing.T) { i := Invoice{InvoiceNumber: "3213121", NetAmount: 2, GrossAmount: 2} tree, err := i.getDocumentDataTree() assert.Nil(t, err, "tree should be generated without error") - _, leaf := tree.GetLeafByProperty("invoice_number") - assert.Equal(t, "invoice_number", leaf.Property) + _, leaf := tree.GetLeafByProperty("invoice.invoice_number") + assert.NotNil(t, leaf) + assert.Equal(t, "invoice.invoice_number", leaf.Property.ReadableName()) } func createMockInvoice(t *testing.T) (*Invoice, *coredocumentpb.CoreDocument, error) { diff --git a/documents/invoice/repository.go b/documents/invoice/repository.go deleted file mode 100644 index 8d5a213dc..000000000 --- a/documents/invoice/repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package invoice - -import ( - "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/storage" -) - -// repository is the invoice repository -type repository struct { - documents.LevelDBRepository -} - -// getRepository returns the implemented documents.legacyRepo for invoices -func getRepository() documents.Repository { - return &repository{ - documents.LevelDBRepository{ - KeyPrefix: "invoice", - LevelDB: storage.GetLevelDBStorage(), - }, - } -} diff --git a/documents/invoice/repository_test.go b/documents/invoice/repository_test.go deleted file mode 100644 index ab66d5859..000000000 --- a/documents/invoice/repository_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// +build unit - -package invoice - -import ( - "testing" - - "github.com/centrifuge/go-centrifuge/utils" - "github.com/stretchr/testify/assert" -) - -func TestRepository(t *testing.T) { - repo := getRepository() - invRepo := repo.(*repository) - assert.Equal(t, invRepo.KeyPrefix, "invoice") - assert.NotNil(t, invRepo.LevelDB, "missing leveldb instance") - - id := utils.RandomSlice(32) - doc := &Invoice{ - InvoiceNumber: "inv1234", - SenderName: "Jack", - SenderZipcode: "921007", - SenderCountry: "AUS", - RecipientName: "John", - RecipientZipcode: "12345", - RecipientCountry: "Germany", - Currency: "EUR", - GrossAmount: 800, - } - - err := repo.Create(id, doc) - assert.Nil(t, err, "create must pass") - - // successful get - getDoc := new(Invoice) - err = repo.LoadByID(id, getDoc) - assert.Nil(t, err, "get must pass") - assert.Equal(t, getDoc, doc, "documents mismatch") - - // successful update - doc.GrossAmount = 200 - err = repo.Update(id, doc) - assert.Nil(t, err, "update must pass") - assert.Nil(t, repo.LoadByID(id, getDoc), "get must pass") - assert.Equal(t, getDoc.GrossAmount, doc.GrossAmount, "amount must match") -} - -func TestRepository_getRepository(t *testing.T) { - r := getRepository() - assert.NotNil(t, r) - assert.Equal(t, "invoice", r.(*repository).KeyPrefix) -} diff --git a/documents/invoice/service.go b/documents/invoice/service.go index aec19c6e0..14958155e 100644 --- a/documents/invoice/service.go +++ b/documents/invoice/service.go @@ -2,24 +2,22 @@ package invoice import ( "bytes" - "context" - "fmt" "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/notification" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/centerrors" - "github.com/centrifuge/go-centrifuge/code" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/coredocument/processor" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" - centED25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/centrifuge/go-centrifuge/notification" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" "github.com/centrifuge/go-centrifuge/signatures" + "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes" logging "github.com/ipfs/go-log" @@ -32,16 +30,16 @@ type Service interface { documents.Service // DeriverFromPayload derives Invoice from clientPayload - DeriveFromCreatePayload(*clientinvoicepb.InvoiceCreatePayload, *documents.ContextHeader) (documents.Model, error) + DeriveFromCreatePayload(*clientinvoicepb.InvoiceCreatePayload, *header.ContextHeader) (documents.Model, error) // DeriveFromUpdatePayload derives invoice model from update payload - DeriveFromUpdatePayload(*clientinvoicepb.InvoiceUpdatePayload, *documents.ContextHeader) (documents.Model, error) + DeriveFromUpdatePayload(*clientinvoicepb.InvoiceUpdatePayload, *header.ContextHeader) (documents.Model, error) // Create validates and persists invoice Model and returns a Updated model - Create(ctx context.Context, inv documents.Model) (documents.Model, error) + Create(ctx *header.ContextHeader, inv documents.Model) (documents.Model, error) // Update validates and updates the invoice model and return the updated model - Update(ctx context.Context, inv documents.Model) (documents.Model, error) + Update(ctx *header.ContextHeader, inv documents.Model) (documents.Model, error) // DeriveInvoiceData returns the invoice data as client data DeriveInvoiceData(inv documents.Model) (*clientinvoicepb.InvoiceData, error) @@ -51,17 +49,19 @@ type Service interface { } // service implements Service and handles all invoice related persistence and validations -// service always returns errors of type `centerrors` with proper error code +// service always returns errors of type `errors.Error` or `errors.TypedError` type service struct { + config documents.Config repo documents.Repository - coreDocProcessor coredocumentprocessor.Processor + coreDocProcessor coredocument.Processor notifier notification.Sender anchorRepository anchors.AnchorRepository + identityService identity.Service } // DefaultService returns the default implementation of the service -func DefaultService(repo documents.Repository, processor coredocumentprocessor.Processor, anchorRepository anchors.AnchorRepository) Service { - return service{repo: repo, coreDocProcessor: processor, notifier: ¬ification.WebhookSender{}, anchorRepository: anchorRepository} +func DefaultService(config config.Configuration, repo documents.Repository, processor coredocument.Processor, anchorRepository anchors.AnchorRepository, identityService identity.Service) Service { + return service{config: config, repo: repo, coreDocProcessor: processor, notifier: notification.NewWebhookSender(config), anchorRepository: anchorRepository, identityService: identityService} } // CreateProofs creates proofs for the latest version document given the fields @@ -88,19 +88,19 @@ func (s service) CreateProofsForVersion(documentID, version []byte, fields []str func (s service) invoiceProof(model documents.Model, fields []string) (*documents.DocumentProof, error) { inv, ok := model.(*Invoice) if !ok { - return nil, centerrors.New(code.DocumentInvalid, "document of invalid type") + return nil, documents.ErrDocumentInvalidType } - if err := coredocument.PostAnchoredValidator(s.anchorRepository).Validate(nil, inv); err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + if err := coredocument.PostAnchoredValidator(s.identityService, s.anchorRepository).Validate(nil, inv); err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } coreDoc, proofs, err := inv.createProofs(fields) if err != nil { - return nil, err + return nil, errors.NewTypedError(documents.ErrDocumentProof, err) } return &documents.DocumentProof{ - DocumentId: coreDoc.DocumentIdentifier, - VersionId: coreDoc.CurrentVersion, + DocumentID: coreDoc.DocumentIdentifier, + VersionID: coreDoc.CurrentVersion, FieldProofs: proofs, }, nil } @@ -110,22 +110,22 @@ func (s service) DeriveFromCoreDocument(cd *coredocumentpb.CoreDocument) (docume var model documents.Model = new(Invoice) err := model.UnpackCoreDocument(cd) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentUnPackingCoreDocument, err) } return model, nil } // UnpackFromCreatePayload initializes the model with parameters provided from the rest-api call -func (s service) DeriveFromCreatePayload(invoiceInput *clientinvoicepb.InvoiceCreatePayload, contextHeader *documents.ContextHeader) (documents.Model, error) { - if invoiceInput == nil { - return nil, centerrors.New(code.DocumentInvalid, "input is nil") +func (s service) DeriveFromCreatePayload(payload *clientinvoicepb.InvoiceCreatePayload, contextHeader *header.ContextHeader) (documents.Model, error) { + if payload == nil || payload.Data == nil { + return nil, documents.ErrDocumentNil } invoiceModel := new(Invoice) - err := invoiceModel.InitInvoiceInput(invoiceInput, contextHeader) + err := invoiceModel.InitInvoiceInput(payload, contextHeader) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } return invoiceModel, nil @@ -135,55 +135,71 @@ func (s service) DeriveFromCreatePayload(invoiceInput *clientinvoicepb.InvoiceCr func (s service) calculateDataRoot(old, new documents.Model, validator documents.Validator) (documents.Model, error) { inv, ok := new.(*Invoice) if !ok { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("unknown document type: %T", new)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalidType, errors.New("unknown document type: %T", new)) } // create data root, has to be done at the model level to access fields err := inv.calculateDataRoot() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } // validate the invoice err = validator.Validate(old, inv) if err != nil { - return nil, centerrors.NewWithErrors(code.DocumentInvalid, "validations failed", documents.ConvertToMap(err)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) + } + + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) } // we use CurrentVersion as the id since that will be unique across multiple versions of the same document - err = s.repo.Create(inv.CoreDocument.CurrentVersion, inv) + err = s.repo.Create(tenantID, inv.CoreDocument.CurrentVersion, inv) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) } return inv, nil } // Create takes and invoice model and does required validation checks, tries to persist to DB -func (s service) Create(ctx context.Context, inv documents.Model) (documents.Model, error) { +func (s service) Create(ctx *header.ContextHeader, inv documents.Model) (documents.Model, error) { inv, err := s.calculateDataRoot(nil, inv, CreateValidator()) if err != nil { return nil, err } - inv, err = documents.AnchorDocument(ctx, inv, s.coreDocProcessor, s.repo.Update) + inv, err = documents.AnchorDocument(ctx, inv, s.coreDocProcessor, s.updater) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, err } return inv, nil } +// updater wraps logic related to updating documents so that it can be executed as a closure +func (s service) updater(id []byte, model documents.Model) error { + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + return s.repo.Update(tenantID, id, model) +} + // Update finds the old document, validates the new version and persists the updated document -func (s service) Update(ctx context.Context, inv documents.Model) (documents.Model, error) { +func (s service) Update(ctx *header.ContextHeader, inv documents.Model) (documents.Model, error) { cd, err := inv.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } old, err := s.GetCurrentVersion(cd.DocumentIdentifier) if err != nil { - return nil, centerrors.New(code.DocumentNotFound, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentNotFound, err) } inv, err = s.calculateDataRoot(old, inv, UpdateValidator()) @@ -191,9 +207,9 @@ func (s service) Update(ctx context.Context, inv documents.Model) (documents.Mod return nil, err } - inv, err = documents.AnchorDocument(ctx, inv, s.coreDocProcessor, s.repo.Update) + inv, err = documents.AnchorDocument(ctx, inv, s.coreDocProcessor, s.updater) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, err } return inv, nil @@ -203,7 +219,7 @@ func (s service) Update(ctx context.Context, inv documents.Model) (documents.Mod func (s service) GetVersion(documentID []byte, version []byte) (doc documents.Model, err error) { inv, err := s.getInvoiceVersion(documentID, version) if err != nil { - return nil, centerrors.Wrap(err, "document not found for the given version") + return nil, err } return inv, nil } @@ -212,34 +228,39 @@ func (s service) GetVersion(documentID []byte, version []byte) (doc documents.Mo func (s service) GetCurrentVersion(documentID []byte) (doc documents.Model, err error) { inv, err := s.getInvoiceVersion(documentID, documentID) if err != nil { - return nil, centerrors.Wrap(err, "document not found") + return nil, errors.NewTypedError(documents.ErrDocumentNotFound, err) } nextVersion := inv.CoreDocument.NextVersion for nextVersion != nil { temp, err := s.getInvoiceVersion(documentID, nextVersion) if err != nil { + // here the err is returned as nil because it is expected that the nextVersion is not available in the db at some stage of the iteration return inv, nil - } else { - inv = temp - nextVersion = inv.CoreDocument.NextVersion } + + inv = temp + nextVersion = inv.CoreDocument.NextVersion } return inv, nil } func (s service) getInvoiceVersion(documentID, version []byte) (inv *Invoice, err error) { - var doc documents.Model = new(Invoice) - err = s.repo.LoadByID(version, doc) + // get tenant ID + tenantID, err := s.config.GetIdentityID() if err != nil { - return nil, err + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + doc, err := s.repo.Get(tenantID, version) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentVersionNotFound, err) } inv, ok := doc.(*Invoice) if !ok { - return nil, err + return nil, documents.ErrDocumentInvalidType } if !bytes.Equal(inv.CoreDocument.DocumentIdentifier, documentID) { - return nil, centerrors.New(code.DocumentInvalid, "version is not valid for this identifier") + return nil, errors.NewTypedError(documents.ErrDocumentVersionNotFound, errors.New("version is not valid for this identifier")) } return inv, nil } @@ -248,19 +269,19 @@ func (s service) getInvoiceVersion(documentID, version []byte) (inv *Invoice, er func (s service) DeriveInvoiceResponse(doc documents.Model) (*clientinvoicepb.InvoiceResponse, error) { cd, err := doc.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } collaborators := make([]string, len(cd.Collaborators)) for i, c := range cd.Collaborators { cid, err := identity.ToCentID(c) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentCollaborator, err) } collaborators[i] = cid.String() } - header := &clientinvoicepb.ResponseHeader{ + h := &clientinvoicepb.ResponseHeader{ DocumentId: hexutil.Encode(cd.DocumentIdentifier), VersionId: hexutil.Encode(cd.CurrentVersion), Collaborators: collaborators, @@ -272,7 +293,7 @@ func (s service) DeriveInvoiceResponse(doc documents.Model) (*clientinvoicepb.In } return &clientinvoicepb.InvoiceResponse{ - Header: header, + Header: h, Data: data, }, nil @@ -282,80 +303,92 @@ func (s service) DeriveInvoiceResponse(doc documents.Model) (*clientinvoicepb.In func (s service) DeriveInvoiceData(doc documents.Model) (*clientinvoicepb.InvoiceData, error) { inv, ok := doc.(*Invoice) if !ok { - return nil, centerrors.New(code.DocumentInvalid, "document of invalid type") + return nil, documents.ErrDocumentInvalidType } - data := inv.getClientData() - return data, nil + return inv.getClientData(), nil } // DeriveFromUpdatePayload returns a new version of the old invoice identified by identifier in payload -func (s service) DeriveFromUpdatePayload(payload *clientinvoicepb.InvoiceUpdatePayload, contextHeader *documents.ContextHeader) (documents.Model, error) { - if payload == nil { - return nil, centerrors.New(code.DocumentInvalid, "invalid payload") +func (s service) DeriveFromUpdatePayload(payload *clientinvoicepb.InvoiceUpdatePayload, contextHeader *header.ContextHeader) (documents.Model, error) { + if payload == nil || payload.Data == nil { + return nil, documents.ErrDocumentNil } // get latest old version of the document id, err := hexutil.Decode(payload.Identifier) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to decode identifier: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentIdentifier, errors.New("failed to decode identifier: %v", err)) } old, err := s.GetCurrentVersion(id) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to fetch old version: %v", err)) + return nil, err } // load invoice data inv := new(Invoice) err = inv.initInvoiceFromData(payload.Data) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to load invoice from data: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, errors.New("failed to load invoice from data: %v", err)) } // update core document oldCD, err := old.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } - collaborators := append([]string{contextHeader.Self().String()}, payload.Collaborators...) + collaborators := append([]string{contextHeader.Self().ID.String()}, payload.Collaborators...) inv.CoreDocument, err = coredocument.PrepareNewVersion(*oldCD, collaborators) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to prepare new version: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentPrepareCoreDocument, err) } return inv, nil } -// RequestDocumentSignature Validates, Signs document received over the p2p layer and returs Signature -func (s service) RequestDocumentSignature(model documents.Model) (*coredocumentpb.Signature, error) { - if err := coredocument.SignatureRequestValidator().Validate(nil, model); err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) +// RequestDocumentSignature Validates, Signs document received over the p2p layer and returns Signature +func (s service) RequestDocumentSignature(contextHeader *header.ContextHeader, model documents.Model) (*coredocumentpb.Signature, error) { + if err := coredocument.SignatureRequestValidator(s.identityService).Validate(nil, model); err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } doc, err := model.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } srvLog.Infof("coredoc received %x with signing root %x", doc.DocumentIdentifier, doc.SigningRoot) - idConfig, err := centED25519.GetIDConfig() - if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get ID Config: %v", err)) + idKeys, ok := contextHeader.Self().Keys[identity.KeyPurposeSigning] + if !ok { + return nil, errors.NewTypedError(documents.ErrDocumentSigning, errors.New("missing signing key")) } - - sig := signatures.Sign(idConfig, doc.SigningRoot) + sig := signatures.Sign(contextHeader.Self().ID[:], idKeys.PrivateKey, idKeys.PublicKey, doc.SigningRoot) doc.Signatures = append(doc.Signatures, sig) err = model.UnpackCoreDocument(doc) if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to Unpack CoreDocument: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentUnPackingCoreDocument, err) } - err = s.repo.Create(doc.CurrentVersion, model) + // get tenant ID + tenantID, err := s.config.GetIdentityID() if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to store document: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + + // Logic for receiving version n (n > 1) of the document for the first time + if !s.repo.Exists(tenantID, doc.DocumentIdentifier) && !utils.IsSameByteSlice(doc.DocumentIdentifier, doc.CurrentVersion) { + err = s.repo.Create(tenantID, doc.DocumentIdentifier, model) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) + } + } + + err = s.repo.Create(tenantID, doc.CurrentVersion, model) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) } srvLog.Infof("signed coredoc %x with version %x", doc.DocumentIdentifier, doc.CurrentVersion) @@ -364,27 +397,33 @@ func (s service) RequestDocumentSignature(model documents.Model) (*coredocumentp // ReceiveAnchoredDocument receives a new anchored document, validates and updates the document in DB func (s service) ReceiveAnchoredDocument(model documents.Model, headers *p2ppb.CentrifugeHeader) error { - if err := coredocument.PostAnchoredValidator(s.anchorRepository).Validate(nil, model); err != nil { - return centerrors.New(code.DocumentInvalid, err.Error()) + if err := coredocument.PostAnchoredValidator(s.identityService, s.anchorRepository).Validate(nil, model); err != nil { + return errors.NewTypedError(documents.ErrDocumentInvalid, err) } doc, err := model.PackCoreDocument() if err != nil { - return centerrors.New(code.DocumentInvalid, err.Error()) + return errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) + } + + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) } - err = s.repo.Update(doc.CurrentVersion, model) + err = s.repo.Update(tenantID, doc.CurrentVersion, model) if err != nil { - return centerrors.New(code.Unknown, err.Error()) + return errors.NewTypedError(documents.ErrDocumentPersistence, err) } ts, _ := ptypes.TimestampProto(time.Now().UTC()) notificationMsg := ¬ificationpb.NotificationMessage{ - EventType: uint32(notification.ReceivedPayload), - CentrifugeId: headers.SenderCentrifugeId, - Recorded: ts, - DocumentType: doc.EmbeddedData.TypeUrl, - DocumentIdentifier: doc.DocumentIdentifier, + EventType: uint32(notification.ReceivedPayload), + CentrifugeId: hexutil.Encode(headers.SenderCentrifugeId), + Recorded: ts, + DocumentType: doc.EmbeddedData.TypeUrl, + DocumentId: hexutil.Encode(doc.DocumentIdentifier), } // Async until we add queuing @@ -392,3 +431,13 @@ func (s service) ReceiveAnchoredDocument(model documents.Model, headers *p2ppb.C return nil } + +// Exists checks if an invoice exists +func (s service) Exists(documentID []byte) bool { + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return false + } + return s.repo.Exists(tenantID, documentID) +} diff --git a/documents/invoice/service_test.go b/documents/invoice/service_test.go index a8f455df5..705f4b1ed 100644 --- a/documents/invoice/service_test.go +++ b/documents/invoice/service_test.go @@ -4,24 +4,22 @@ package invoice import ( "context" - "fmt" "math/big" - "strconv" "testing" - "github.com/centrifuge/go-centrifuge/testingutils" - "github.com/centrifuge/go-centrifuge/testingutils/commons" - "github.com/centrifuge/go-centrifuge/testingutils/documents" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/code" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" clientinvoicepb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" - "github.com/centrifuge/go-centrifuge/signatures" + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" + "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" @@ -29,9 +27,9 @@ import ( ) var ( - centID = utils.RandomSlice(identity.CentIDLength) - 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} + centIDBytes = utils.RandomSlice(identity.CentIDLength) + 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} ) type mockAnchorRepo struct { @@ -39,19 +37,25 @@ type mockAnchorRepo struct { anchors.AnchorRepository } -func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocRoot, error) { +func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocRoot) + docRoot, _ := args.Get(0).(anchors.DocumentRoot) return docRoot, args.Error(1) } func TestDefaultService(t *testing.T) { - srv := DefaultService(getRepository(), &testingutils.MockCoreDocumentProcessor{}, nil) + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil).Once() + srv := DefaultService(c, testRepo(), &testingcoredocument.MockCoreDocumentProcessor{}, nil, nil) assert.NotNil(t, srv, "must be non-nil") } -func getServiceWithMockedLayers() Service { - return DefaultService(getRepository(), &testingutils.MockCoreDocumentProcessor{}, &mockAnchorRepo{}) +func getServiceWithMockedLayers() (testingcommons.MockIDService, Service) { + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + idService := testingcommons.MockIDService{} + idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + return idService, DefaultService(c, testRepo(), &testingcoredocument.MockCoreDocumentProcessor{}, &mockAnchorRepo{}, &idService) } func createMockDocument() (*Invoice, error) { @@ -66,13 +70,13 @@ func createMockDocument() (*Invoice, error) { NextVersion: nextIdentifier, }, } - err := getRepository().Create(documentIdentifier, inv1) + err := testRepo().Create(centIDBytes, documentIdentifier, inv1) return inv1, err } func TestService_DeriveFromCoreDocument(t *testing.T) { // nil doc - invSrv := service{repo: getRepository()} + invSrv := service{repo: testRepo()} _, err := invSrv.DeriveFromCoreDocument(nil) assert.Error(t, err, "must fail to derive") @@ -91,7 +95,7 @@ func TestService_DeriveFromCoreDocument(t *testing.T) { } func TestService_DeriveFromPayload(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() payload := testingdocuments.CreateInvoicePayload() var model documents.Model var err error @@ -100,7 +104,11 @@ func TestService_DeriveFromPayload(t *testing.T) { _, err = invSrv.DeriveFromCreatePayload(nil, nil) assert.Error(t, err, "DeriveWithInvoiceInput should produce an error if invoiceInput equals nil") - contextHeader, err := documents.NewContextHeader() + // fail due to nil payload data + _, err = invSrv.DeriveFromCreatePayload(&clientinvoicepb.InvoiceCreatePayload{}, nil) + assert.Error(t, err, "DeriveWithInvoiceInput should produce an error if invoiceInput equals nil") + + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) model, err = invSrv.DeriveFromCreatePayload(payload, contextHeader) assert.Nil(t, err, "valid invoiceData shouldn't produce an error") @@ -111,7 +119,7 @@ func TestService_DeriveFromPayload(t *testing.T) { } func TestService_GetLastVersion(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() thirdIdentifier := utils.RandomSlice(32) doc, err := createMockDocument() assert.Nil(t, err) @@ -131,7 +139,7 @@ func TestService_GetLastVersion(t *testing.T) { }, } - err = getRepository().Create(doc.CoreDocument.NextVersion, inv2) + err = testRepo().Create(centIDBytes, doc.CoreDocument.NextVersion, inv2) assert.Nil(t, err) mod2, err := invSrv.GetCurrentVersion(doc.CoreDocument.DocumentIdentifier) @@ -143,7 +151,7 @@ func TestService_GetLastVersion(t *testing.T) { } func TestService_GetVersion_invalid_version(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() currentVersion := utils.RandomSlice(32) inv := &Invoice{ GrossAmount: 60, @@ -152,16 +160,16 @@ func TestService_GetVersion_invalid_version(t *testing.T) { CurrentVersion: currentVersion, }, } - err := getRepository().Create(currentVersion, inv) + err := testRepo().Create(centIDBytes, currentVersion, inv) assert.Nil(t, err) mod, err := invSrv.GetVersion(utils.RandomSlice(32), currentVersion) - assert.EqualError(t, err, "[4]document not found for the given version: version is not valid for this identifier") + assert.True(t, errors.IsOfType(documents.ErrDocumentVersionNotFound, err)) assert.Nil(t, mod) } func TestService_GetVersion(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() documentIdentifier := utils.RandomSlice(32) currentVersion := utils.RandomSlice(32) inv := &Invoice{ @@ -171,7 +179,7 @@ func TestService_GetVersion(t *testing.T) { CurrentVersion: currentVersion, }, } - err := getRepository().Create(currentVersion, inv) + err := testRepo().Create(centIDBytes, currentVersion, inv) assert.Nil(t, err) mod, err := invSrv.GetVersion(documentIdentifier, currentVersion) @@ -184,14 +192,35 @@ func TestService_GetVersion(t *testing.T) { assert.Error(t, err) } +func TestService_Exists(t *testing.T) { + _, invSrv := getServiceWithMockedLayers() + documentIdentifier := utils.RandomSlice(32) + inv := &Invoice{ + GrossAmount: 60, + CoreDocument: &coredocumentpb.CoreDocument{ + DocumentIdentifier: documentIdentifier, + CurrentVersion: documentIdentifier, + }, + } + err := testRepo().Create(centIDBytes, documentIdentifier, inv) + assert.Nil(t, err) + + exists := invSrv.Exists(documentIdentifier) + assert.True(t, exists, "invoice should exist") + + exists = invSrv.Exists(utils.RandomSlice(32)) + assert.False(t, exists, "invoice should not exist") + +} + func TestService_Create(t *testing.T) { - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) - invSrv := service{repo: getRepository()} - ctx := context.Background() + _, srv := getServiceWithMockedLayers() + invSrv := srv.(service) // calculate data root fails - m, err := invSrv.Create(context.Background(), &testingdocuments.MockModel{}) + m, err := invSrv.Create(ctxh, &testingdocuments.MockModel{}) assert.Nil(t, m) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown document type") @@ -199,10 +228,10 @@ func TestService_Create(t *testing.T) { // anchor fails po, err := invSrv.DeriveFromCreatePayload(testingdocuments.CreateInvoicePayload(), ctxh) assert.Nil(t, err) - proc := &testingutils.MockCoreDocumentProcessor{} - proc.On("PrepareForSignatureRequests", po).Return(fmt.Errorf("anchoring failed")).Once() + proc := &testingcoredocument.MockCoreDocumentProcessor{} + proc.On("PrepareForSignatureRequests", po).Return(errors.New("anchoring failed")).Once() invSrv.coreDocProcessor = proc - m, err = invSrv.Create(ctx, po) + m, err = invSrv.Create(ctxh, po) proc.AssertExpectations(t) assert.Nil(t, m) assert.Error(t, err) @@ -211,25 +240,25 @@ func TestService_Create(t *testing.T) { // success po, err = invSrv.DeriveFromCreatePayload(testingdocuments.CreateInvoicePayload(), ctxh) assert.Nil(t, err) - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", po).Return(nil).Once() - proc.On("RequestSignatures", ctx, po).Return(nil).Once() + proc.On("RequestSignatures", ctxh, po).Return(nil).Once() proc.On("PrepareForAnchoring", po).Return(nil).Once() proc.On("AnchorDocument", po).Return(nil).Once() - proc.On("SendDocument", ctx, po).Return(nil).Once() + proc.On("SendDocument", ctxh, po).Return(nil).Once() invSrv.coreDocProcessor = proc - m, err = invSrv.Create(ctx, po) + m, err = invSrv.Create(ctxh, po) proc.AssertExpectations(t) assert.Nil(t, err) newCD, err := m.PackCoreDocument() assert.Nil(t, err) - assert.True(t, getRepository().Exists(newCD.DocumentIdentifier)) - assert.True(t, getRepository().Exists(newCD.CurrentVersion)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.DocumentIdentifier)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.CurrentVersion)) } func TestService_DeriveInvoiceData(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() // some random model _, err := invSrv.DeriveInvoiceData(&mockModel{}) @@ -237,7 +266,7 @@ func TestService_DeriveInvoiceData(t *testing.T) { // success payload := testingdocuments.CreateInvoicePayload() - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) inv, err := invSrv.DeriveFromCreatePayload(payload, contextHeader) assert.Nil(t, err, "must be non nil") @@ -248,9 +277,9 @@ func TestService_DeriveInvoiceData(t *testing.T) { func TestService_DeriveInvoiceResponse(t *testing.T) { // success - invSrv := service{repo: getRepository()} + invSrv := service{repo: testRepo()} payload := testingdocuments.CreateInvoicePayload() - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) inv1, err := invSrv.DeriveFromCreatePayload(payload, contextHeader) assert.Nil(t, err, "must be non nil") @@ -266,116 +295,101 @@ func TestService_DeriveInvoiceResponse(t *testing.T) { } // Functions returns service mocks -func mockSignatureCheck(i *Invoice, invSrv Service) identity.Service { +func mockSignatureCheck(i *Invoice, idService testingcommons.MockIDService, invSrv Service) testingcommons.MockIDService { idkey := &identity.EthereumIdentityKey{ Key: key1Pub, Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, RevokedAt: big.NewInt(0), } - anchorID, _ := anchors.NewAnchorID(i.CoreDocument.DocumentIdentifier) - docRoot, _ := anchors.NewDocRoot(i.CoreDocument.DocumentRoot) + anchorID, _ := anchors.ToAnchorID(i.CoreDocument.DocumentIdentifier) + docRoot, _ := anchors.ToDocumentRoot(i.CoreDocument.DocumentRoot) mockRepo := invSrv.(service).anchorRepository.(*mockAnchorRepo) mockRepo.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() - srv := &testingcommons.MockIDService{} id := &testingcommons.MockID{} - centID, _ := identity.ToCentID(centID) - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() + centID, _ := identity.ToCentID(centIDBytes) + idService.On("LookupIdentityForID", centID).Return(id, nil).Once() id.On("FetchKey", key1Pub[:]).Return(idkey, nil).Once() - return srv -} - -func setIdentityService(idService identity.Service) { - identity.IDService = idService + return idService } func TestService_CreateProofs(t *testing.T) { - defer setIdentityService(identity.IDService) - invSrv := getServiceWithMockedLayers() + idService, invSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, invSrv) - setIdentityService(idService) - proof, err := invSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invoice_number"}) + idService = mockSignatureCheck(i, idService, invSrv) + proof, err := invSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invoice.invoice_number"}) assert.Nil(t, err) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentId) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.VersionId) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentID) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.VersionID) assert.Equal(t, len(proof.FieldProofs), 1) - assert.Equal(t, proof.FieldProofs[0].GetProperty(), "invoice_number") + assert.Equal(t, proof.FieldProofs[0].GetReadableName(), "invoice.invoice_number") } func TestService_CreateProofsValidationFails(t *testing.T) { - defer setIdentityService(identity.IDService) - invSrv := getServiceWithMockedLayers() + idService, invSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) i.CoreDocument.SigningRoot = nil - err = getRepository().Update(i.CoreDocument.CurrentVersion, i) + err = testRepo().Update(centIDBytes, i.CoreDocument.CurrentVersion, i) assert.Nil(t, err) - idService := mockSignatureCheck(i, invSrv) - setIdentityService(idService) - _, err = invSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invoice_number"}) + idService = mockSignatureCheck(i, idService, invSrv) + _, err = invSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invoice.invoice_number"}) assert.NotNil(t, err) assert.Contains(t, err.Error(), "signing root missing") } func TestService_CreateProofsInvalidField(t *testing.T) { - defer setIdentityService(identity.IDService) - invSrv := getServiceWithMockedLayers() + idService, invSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, invSrv) - setIdentityService(idService) + idService = mockSignatureCheck(i, idService, invSrv) _, err = invSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invalid_field"}) assert.Error(t, err) - assert.Equal(t, "createProofs error No such field: invalid_field in obj", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentProof, err)) } func TestService_CreateProofsDocumentDoesntExist(t *testing.T) { - invService := getServiceWithMockedLayers() - _, err := invService.CreateProofs(utils.RandomSlice(32), []string{"invoice_number"}) + _, invService := getServiceWithMockedLayers() + _, err := invService.CreateProofs(utils.RandomSlice(32), []string{"invoice.invoice_number"}) assert.Error(t, err) - assert.Equal(t, "document not found: leveldb: not found", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentNotFound, err)) } func TestService_CreateProofsForVersion(t *testing.T) { - defer setIdentityService(identity.IDService) - invSrv := getServiceWithMockedLayers() + idService, invSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, invSrv) - setIdentityService(idService) + idService = mockSignatureCheck(i, idService, invSrv) olderVersion := i.CoreDocument.CurrentVersion i, err = updatedAnchoredMockDocument(t, i) assert.Nil(t, err) - proof, err := invSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, olderVersion, []string{"invoice_number"}) + proof, err := invSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, olderVersion, []string{"invoice.invoice_number"}) assert.Nil(t, err) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentId) - assert.Equal(t, olderVersion, proof.VersionId) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentID) + assert.Equal(t, olderVersion, proof.VersionID) assert.Equal(t, len(proof.FieldProofs), 1) - assert.Equal(t, proof.FieldProofs[0].GetProperty(), "invoice_number") + assert.Equal(t, proof.FieldProofs[0].GetReadableName(), "invoice.invoice_number") } func TestService_CreateProofsForVersionDocumentDoesntExist(t *testing.T) { i, err := createAnchoredMockDocument(t, false) - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() assert.Nil(t, err) - _, err = invSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, utils.RandomSlice(32), []string{"invoice_number"}) + _, err = invSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, utils.RandomSlice(32), []string{"invoice.invoice_number"}) assert.Error(t, err) - assert.Equal(t, "document not found for the given version: leveldb: not found", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentVersionNotFound, err)) } func TestService_RequestDocumentSignature_SigningRootNil(t *testing.T) { - defer setIdentityService(identity.IDService) - invSrv := getServiceWithMockedLayers() + idService, invSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, true) assert.Nil(t, err) - idService := mockSignatureCheck(i, invSrv) - setIdentityService(idService) + idService = mockSignatureCheck(i, idService, invSrv) i.CoreDocument.SigningRoot = nil - signature, err := invSrv.RequestDocumentSignature(i) + ctxh, err := header.NewContextHeader(context.Background(), cfg) + signature, err := invSrv.RequestDocumentSignature(ctxh, i) assert.NotNil(t, err) - assert.Contains(t, err.Error(), strconv.Itoa(int(code.DocumentInvalid))) - assert.Contains(t, err.Error(), "signing root missing") + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalid, err)) assert.Nil(t, signature) } @@ -400,11 +414,20 @@ func createAnchoredMockDocument(t *testing.T, skipSave bool) (*Invoice, error) { return nil, err } - sig := signatures.Sign(&config.IdentityConfig{ - ID: centID, + centID, err := identity.ToCentID(centIDBytes) + assert.Nil(t, err) + signKey := identity.IDKey{ PublicKey: key1Pub[:], PrivateKey: key1, - }, corDoc.SigningRoot) + } + idConfig := &identity.IDConfig{ + ID: centID, + Keys: map[int]identity.IDKey{ + identity.KeyPurposeSigning: signKey, + }, + } + + sig := identity.Sign(idConfig, identity.KeyPurposeSigning, corDoc.SigningRoot) corDoc.Signatures = append(corDoc.Signatures, sig) @@ -418,7 +441,7 @@ func createAnchoredMockDocument(t *testing.T, skipSave bool) (*Invoice, error) { } if !skipSave { - err = getRepository().Create(i.CoreDocument.CurrentVersion, i) + err = testRepo().Create(centIDBytes, i.CoreDocument.CurrentVersion, i) if err != nil { return nil, err } @@ -456,7 +479,7 @@ func updatedAnchoredMockDocument(t *testing.T, i *Invoice) (*Invoice, error) { if err != nil { return nil, err } - err = getRepository().Create(i.CoreDocument.CurrentVersion, i) + err = testRepo().Create(centIDBytes, i.CoreDocument.CurrentVersion, i) if err != nil { return nil, err } @@ -464,19 +487,26 @@ func updatedAnchoredMockDocument(t *testing.T, i *Invoice) (*Invoice, error) { } func TestService_DeriveFromUpdatePayload(t *testing.T) { - invSrv := getServiceWithMockedLayers() + _, invSrv := getServiceWithMockedLayers() // nil payload doc, err := invSrv.DeriveFromUpdatePayload(nil, nil) assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid payload") + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) + assert.Nil(t, doc) + + // nil payload data + doc, err = invSrv.DeriveFromUpdatePayload(&clientinvoicepb.InvoiceUpdatePayload{}, nil) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) assert.Nil(t, doc) // messed up identifier - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) - payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "some identifier"} + payload := &clientinvoicepb.InvoiceUpdatePayload{Identifier: "some identifier", Data: &clientinvoicepb.InvoiceData{}} doc, err = invSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentIdentifier, err)) assert.Contains(t, err.Error(), "failed to decode identifier") assert.Nil(t, doc) @@ -485,7 +515,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { payload.Identifier = hexutil.Encode(id) doc, err = invSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to fetch old version") + assert.True(t, errors.IsOfType(documents.ErrDocumentNotFound, err)) assert.Nil(t, doc) // failed to load from data @@ -495,7 +525,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { old.CoreDocument.DocumentIdentifier = id old.CoreDocument.CurrentVersion = id old.CoreDocument.DocumentRoot = utils.RandomSlice(32) - err = getRepository().Create(id, old) + err = testRepo().Create(centIDBytes, id, old) assert.Nil(t, err) payload.Data = &clientinvoicepb.InvoiceData{ Sender: "0x010101010101", @@ -507,7 +537,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { } doc, err = invSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to load invoice from data") + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalid, err)) assert.Nil(t, doc) // failed core document new version @@ -515,7 +545,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { payload.Collaborators = []string{"some wrong ID"} doc, err = invSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to prepare new version") + assert.True(t, errors.IsOfType(documents.ErrDocumentPrepareCoreDocument, err)) assert.Nil(t, doc) // success @@ -539,15 +569,15 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { } func TestService_Update(t *testing.T) { - invSrv := service{repo: getRepository()} - ctx := context.Background() - ctxh, err := documents.NewContextHeader() + _, srv := getServiceWithMockedLayers() + invSrv := srv.(service) + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // pack failed model := &mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("pack error")).Once() - _, err = invSrv.Update(ctx, model) + model.On("PackCoreDocument").Return(nil, errors.New("pack error")).Once() + _, err = invSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "pack error") @@ -556,7 +586,7 @@ func TestService_Update(t *testing.T) { model = &mockModel{} cd := coredocument.New() model.On("PackCoreDocument").Return(cd, nil).Once() - _, err = invSrv.Update(ctx, model) + _, err = invSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "document not found") @@ -569,12 +599,12 @@ func TestService_Update(t *testing.T) { assert.Nil(t, err) cd.DocumentRoot = utils.RandomSlice(32) inv.(*Invoice).CoreDocument = cd - getRepository().Create(cd.CurrentVersion, inv) + testRepo().Create(centIDBytes, cd.CurrentVersion, inv) // calculate data root fails model = &mockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() - _, err = invSrv.Update(ctx, model) + _, err = invSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown document type") @@ -594,23 +624,23 @@ func TestService_Update(t *testing.T) { newData, err := invSrv.DeriveInvoiceData(newInv) assert.Nil(t, err) assert.Equal(t, data, newData) - proc := &testingutils.MockCoreDocumentProcessor{} + proc := &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", newInv).Return(nil).Once() - proc.On("RequestSignatures", ctx, newInv).Return(nil).Once() + proc.On("RequestSignatures", ctxh, newInv).Return(nil).Once() proc.On("PrepareForAnchoring", newInv).Return(nil).Once() proc.On("AnchorDocument", newInv).Return(nil).Once() - proc.On("SendDocument", ctx, newInv).Return(nil).Once() + proc.On("SendDocument", ctxh, newInv).Return(nil).Once() invSrv.coreDocProcessor = proc - inv, err = invSrv.Update(ctx, newInv) + inv, err = invSrv.Update(ctxh, newInv) proc.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, inv) newCD, err := inv.PackCoreDocument() assert.Nil(t, err) - assert.True(t, getRepository().Exists(newCD.DocumentIdentifier)) - assert.True(t, getRepository().Exists(newCD.CurrentVersion)) - assert.True(t, getRepository().Exists(newCD.PreviousVersion)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.DocumentIdentifier)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.CurrentVersion)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.PreviousVersion)) newData, err = invSrv.DeriveInvoiceData(inv) assert.Nil(t, err) @@ -619,8 +649,9 @@ func TestService_Update(t *testing.T) { } func TestService_calculateDataRoot(t *testing.T) { - invSrv := getServiceWithMockedLayers().(service) - ctxh, err := documents.NewContextHeader() + _, srv := getServiceWithMockedLayers() + invSrv := srv.(service) + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // type mismatch @@ -634,7 +665,7 @@ func TestService_calculateDataRoot(t *testing.T) { assert.Nil(t, err) assert.Nil(t, inv.(*Invoice).CoreDocument.DataRoot) v := documents.ValidatorFunc(func(_, _ documents.Model) error { - return fmt.Errorf("validations fail") + return errors.New("validations fail") }) inv, err = invSrv.calculateDataRoot(nil, inv, v) assert.Nil(t, inv) @@ -645,12 +676,12 @@ func TestService_calculateDataRoot(t *testing.T) { inv, err = invSrv.DeriveFromCreatePayload(testingdocuments.CreateInvoicePayload(), ctxh) assert.Nil(t, err) assert.Nil(t, inv.(*Invoice).CoreDocument.DataRoot) - err = invSrv.repo.Create(inv.(*Invoice).CoreDocument.CurrentVersion, inv) + err = invSrv.repo.Create(centIDBytes, inv.(*Invoice).CoreDocument.CurrentVersion, inv) assert.Nil(t, err) inv, err = invSrv.calculateDataRoot(nil, inv, CreateValidator()) assert.Nil(t, inv) assert.Error(t, err) - assert.Contains(t, err.Error(), "document already exists") + assert.Contains(t, err.Error(), "document repository found an already existing model when saving") // success inv, err = invSrv.DeriveFromCreatePayload(testingdocuments.CreateInvoicePayload(), ctxh) @@ -661,3 +692,17 @@ func TestService_calculateDataRoot(t *testing.T) { assert.NotNil(t, inv) assert.NotNil(t, inv.(*Invoice).CoreDocument.DataRoot) } + +var testRepoGlobal documents.Repository + +func testRepo() documents.Repository { + if testRepoGlobal == nil { + ldb, err := storage.NewLevelDBStorage(storage.GetRandomTestStoragePath()) + if err != nil { + panic(err) + } + testRepoGlobal = documents.NewLevelDBRepository(ldb) + testRepoGlobal.Register(&Invoice{}) + } + return testRepoGlobal +} diff --git a/documents/invoice/validator.go b/documents/invoice/validator.go index c012501eb..9dd89a909 100644 --- a/documents/invoice/validator.go +++ b/documents/invoice/validator.go @@ -1,10 +1,9 @@ package invoice import ( - "fmt" - "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" ) @@ -12,17 +11,17 @@ import ( func fieldValidator() documents.Validator { return documents.ValidatorFunc(func(_, new documents.Model) error { if new == nil { - return fmt.Errorf("nil document") + return errors.New("nil document") } inv, ok := new.(*Invoice) if !ok { - return fmt.Errorf("unknown document type") + return errors.New("unknown document type") } var err error if !documents.IsCurrencyValid(inv.Currency) { - err = documents.AppendError(err, documents.NewError("inv_currency", "currency is invalid")) + err = errors.AppendError(err, documents.NewError("inv_currency", "currency is invalid")) } return err @@ -34,34 +33,34 @@ func dataRootValidator() documents.Validator { return documents.ValidatorFunc(func(_, model documents.Model) (err error) { defer func() { if err != nil { - err = fmt.Errorf("data root validation failed: %v", err) + err = errors.New("data root validation failed: %v", err) } }() if model == nil { - return fmt.Errorf("nil document") + return errors.New("nil document") } coreDoc, err := model.PackCoreDocument() if err != nil { - return fmt.Errorf("failed to pack coredocument: %v", err) + return errors.New("failed to pack coredocument: %v", err) } if utils.IsEmptyByteSlice(coreDoc.DataRoot) { - return fmt.Errorf("data root missing") + return errors.New("data root missing") } inv, ok := model.(*Invoice) if !ok { - return fmt.Errorf("unknown document type: %T", model) + return errors.New("unknown document type: %T", model) } if err = inv.calculateDataRoot(); err != nil { - return fmt.Errorf("failed to calculate data root: %v", err) + return errors.New("failed to calculate data root: %v", err) } if !utils.IsSameByteSlice(inv.CoreDocument.DataRoot, coreDoc.DataRoot) { - return fmt.Errorf("mismatched data root") + return errors.New("mismatched data root") } return nil diff --git a/documents/invoice/validator_test.go b/documents/invoice/validator_test.go index 579254d84..4ecc74d0c 100644 --- a/documents/invoice/validator_test.go +++ b/documents/invoice/validator_test.go @@ -3,11 +3,13 @@ package invoice import ( - "fmt" "testing" + "context" + "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" @@ -19,21 +21,21 @@ func TestFieldValidator_Validate(t *testing.T) { // nil error err := fv.Validate(nil, nil) assert.Error(t, err) - errs := documents.Errors(err) + errs := errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be one") assert.Contains(t, errs[0].Error(), "nil document") // unknown type err = fv.Validate(nil, &mockModel{}) assert.Error(t, err) - errs = documents.Errors(err) + errs = errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be one") assert.Contains(t, errs[0].Error(), "unknown document type") // fail err = fv.Validate(nil, new(Invoice)) assert.Error(t, err) - errs = documents.Errors(err) + errs = errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be 2") assert.Contains(t, errs[0].Error(), "currency is invalid") @@ -54,7 +56,7 @@ func TestDataRootValidation_Validate(t *testing.T) { // pack coredoc failed model := &mockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() err = drv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) @@ -79,7 +81,7 @@ func TestDataRootValidation_Validate(t *testing.T) { assert.Contains(t, err.Error(), "unknown document type") // mismatch - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) inv := new(Invoice) err = inv.InitInvoiceInput(testingdocuments.CreateInvoicePayload(), contextHeader) diff --git a/documents/leveldb.go b/documents/leveldb.go deleted file mode 100644 index 65cbfb0e9..000000000 --- a/documents/leveldb.go +++ /dev/null @@ -1,85 +0,0 @@ -package documents - -import ( - "fmt" - - "github.com/syndtr/goleveldb/leveldb" -) - -// LevelDBRepository is implements repository -type LevelDBRepository struct { - KeyPrefix string - LevelDB *leveldb.DB -} - -// Exists returns if the document exists in the repository -func (repo LevelDBRepository) Exists(id []byte) bool { - _, err := repo.LevelDB.Get(repo.GetKey(id), nil) - if err != nil { - return false - } - - return true -} - -// GetKey prepends the id with prefix and returns the result -func (repo LevelDBRepository) GetKey(id []byte) []byte { - return append([]byte(repo.KeyPrefix), id...) -} - -// LoadByID finds the document by id and marshals into message -func (repo LevelDBRepository) LoadByID(id []byte, model Model) error { - if model == nil { - return fmt.Errorf("nil document provided") - } - - data, err := repo.LevelDB.Get(repo.GetKey(id), nil) - if err != nil { - return err - } - - err = model.FromJSON(data) - if err != nil { - return err - } - - return nil -} - -// Create creates the document if not exists -// errors out if document exists -func (repo LevelDBRepository) Create(id []byte, model Model) error { - if model == nil { - return fmt.Errorf("nil model provided") - } - - if repo.Exists(id) { - return fmt.Errorf("document already exists") - } - - data, err := model.JSON() - if err != nil { - return err - } - - return repo.LevelDB.Put(repo.GetKey(id), data, nil) -} - -// Update updates the doc with ID if exists -// errors out if the document -func (repo LevelDBRepository) Update(id []byte, model Model) error { - if model == nil { - return fmt.Errorf("nil document provided") - } - - if !repo.Exists(id) { - return fmt.Errorf("document doesn't exists") - } - - data, err := model.JSON() - if err != nil { - return err - } - - return repo.LevelDB.Put(repo.GetKey(id), data, nil) -} diff --git a/documents/leveldb_test.go b/documents/leveldb_test.go deleted file mode 100644 index 3ea05f7ed..000000000 --- a/documents/leveldb_test.go +++ /dev/null @@ -1,126 +0,0 @@ -// +build unit - -package documents - -import ( - "encoding/json" - "flag" - "fmt" - "os" - "reflect" - "testing" - - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/context/testlogging" - "github.com/centrifuge/go-centrifuge/storage" - "github.com/centrifuge/go-centrifuge/utils" - "github.com/stretchr/testify/assert" -) - -func TestMain(m *testing.M) { - ibootstappers := []bootstrap.TestBootstrapper{ - &testlogging.TestLoggingBootstrapper{}, - &config.Bootstrapper{}, - &storage.Bootstrapper{}, - &Bootstrapper{}, - } - bootstrap.RunTestBootstrappers(ibootstappers, nil) - flag.Parse() - result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) - os.Exit(result) -} - -type model struct { - shouldError bool - Data string `json:"data"` -} - -func (m *model) ID() ([]byte, error) { return []byte{}, nil } -func (m *model) Type() reflect.Type { return reflect.TypeOf(m) } -func (m *model) PackCoreDocument() (*coredocumentpb.CoreDocument, error) { return nil, nil } -func (m *model) UnpackCoreDocument(cd *coredocumentpb.CoreDocument) error { return nil } - -func (m *model) JSON() ([]byte, error) { - if m.shouldError { - return nil, fmt.Errorf("failed to marshal") - } - - return json.Marshal(m) -} - -func (m *model) FromJSON(data []byte) error { - if m.shouldError { - return fmt.Errorf("failed to unmarshal") - } - - return json.Unmarshal(data, m) -} - -func TestDefaultLevelDB_LoadByID(t *testing.T) { - id := utils.RandomSlice(32) - - // missing ID - err := testLevelDB.LoadByID(id, new(model)) - assert.Error(t, err, "error must be non nil") - - // nil document - err = testLevelDB.LoadByID(id, nil) - assert.Error(t, err, "error must be non nil") - - // Failed unmarshal - m := &model{shouldError: true} - err = testLevelDB.LoadByID(id, m) - assert.Error(t, err, "error must be non nil") - - // success - m = &model{Data: "hello, world"} - err = testLevelDB.Create(id, m) - assert.Nil(t, err, "error should be nil") - nm := new(model) - err = testLevelDB.LoadByID(id, nm) - assert.Nil(t, err, "error should be nil") - assert.Equal(t, m, nm, "models must match") -} - -func TestDefaultLevelDB_Create(t *testing.T) { - id := utils.RandomSlice(32) - d := &model{Data: "Create it"} - err := testLevelDB.Create(id, d) - assert.Nil(t, err, "create must pass") - - // same id - err = testLevelDB.Create(id, new(model)) - assert.Error(t, err, "create must fail") - - // nil model - err = testLevelDB.Create(id, nil) - assert.Error(t, err, "create must fail") -} - -func TestDefaultLevelDB_UpdateModel(t *testing.T) { - id := utils.RandomSlice(32) - - // missing Id - err := testLevelDB.Update(id, new(model)) - assert.Error(t, err, "update must fail") - - // nil model - err = testLevelDB.Update(id, nil) - assert.Error(t, err, "update must fail") - - m := &model{Data: "create it"} - err = testLevelDB.Create(id, m) - assert.Nil(t, err, "create must pass") - - // successful one - m.Data = "update it" - err = testLevelDB.Update(id, m) - assert.Nil(t, err, "update must pass") - nm := new(model) - err = testLevelDB.LoadByID(id, nm) - assert.Nil(t, err, "get mode must pass") - assert.Equal(t, m, nm) -} diff --git a/documents/model.go b/documents/model.go index 2f2f682e4..22e9146f1 100644 --- a/documents/model.go +++ b/documents/model.go @@ -1,12 +1,9 @@ package documents import ( - "fmt" "reflect" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" ) // Model is an interface to abstract away model specificness like invoice or purchaseOrder @@ -33,28 +30,3 @@ type Model interface { // FromJSON initialize the model with a json FromJSON(json []byte) error } - -// Placeholder to pass custom request objects down the pipeline -type ContextHeader struct { - self identity.CentID -} - -// NewContextHeader creates new instance of the request headers needed -func NewContextHeader() (*ContextHeader, error) { - idConfig, err := ed25519.GetIDConfig() - if err != nil { - return nil, fmt.Errorf("failed to get id config: %v", err) - } - - self, err := identity.ToCentID(idConfig.ID) - if err != nil { - return nil, fmt.Errorf("failed to convert self to centID: %v", err) - } - - return &ContextHeader{self: self}, nil -} - -// Self returns Self CentID -func (h *ContextHeader) Self() identity.CentID { - return h.self -} diff --git a/documents/model_test.go b/documents/model_test.go new file mode 100644 index 000000000..0a54b681a --- /dev/null +++ b/documents/model_test.go @@ -0,0 +1,28 @@ +// +build unit + +package documents + +import ( + "os" + "testing" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/context/testlogging" + "github.com/centrifuge/go-centrifuge/storage" +) + +var ctx = map[string]interface{}{} + +func TestMain(m *testing.M) { + ibootstappers := []bootstrap.TestBootstrapper{ + &testlogging.TestLoggingBootstrapper{}, + &config.Bootstrapper{}, + &storage.Bootstrapper{}, + &Bootstrapper{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} diff --git a/documents/purchaseorder/bootstrapper.go b/documents/purchaseorder/bootstrapper.go index f84205219..fa50503fd 100644 --- a/documents/purchaseorder/bootstrapper.go +++ b/documents/purchaseorder/bootstrapper.go @@ -1,31 +1,59 @@ package purchaseorder import ( - "errors" - "fmt" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/go-centrifuge/anchors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/coredocument/processor" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/p2p" ) -type Bootstrapper struct { -} +// Bootstrapper implements bootstrap.Bootstrapper. +type Bootstrapper struct{} + +// Bootstrap initialises required services for purchaseorder. +func (Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + if _, ok := ctx[bootstrap.BootstrappedConfig]; !ok { + return errors.New("config hasn't been initialized") + } + cfg := ctx[bootstrap.BootstrappedConfig].(config.Configuration) + + p2pClient, ok := ctx[p2p.BootstrappedP2PClient].(p2p.Client) + if !ok { + return errors.New("p2p client not initialised") + } + + registry, ok := ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("service registry not initialised") + } + + anchorRepo, ok := ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) + if !ok { + return errors.New("anchor repository not initialised") + } + + idService, ok := ctx[identity.BootstrappedIDService].(identity.Service) + if !ok { + return errors.New("identity service not initialised") + } -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - if _, ok := context[bootstrap.BootstrappedLevelDb]; !ok { - return errors.New("could not initialize purchase order repository") + repo, ok := ctx[documents.BootstrappedDocumentRepository].(documents.Repository) + if !ok { + return errors.New("document db repository not initialised") } + repo.Register(&PurchaseOrder{}) // register service - srv := DefaultService(getRepository(), coredocumentprocessor.DefaultProcessor(identity.IDService, p2p.NewP2PClient(), anchors.GetAnchorRepository()), anchors.GetAnchorRepository()) - err := documents.GetRegistryInstance().Register(documenttypes.PurchaseOrderDataTypeUrl, srv) + srv := DefaultService(cfg, repo, coredocument.DefaultProcessor(idService, p2pClient, anchorRepo, cfg), anchorRepo, idService) + err := registry.Register(documenttypes.PurchaseOrderDataTypeUrl, srv) if err != nil { - return fmt.Errorf("failed to register purchase order service") + return errors.New("failed to register purchase order service") } return nil diff --git a/documents/purchaseorder/bootstrapper_test.go b/documents/purchaseorder/bootstrapper_test.go index fd28ca6a5..b6911430f 100644 --- a/documents/purchaseorder/bootstrapper_test.go +++ b/documents/purchaseorder/bootstrapper_test.go @@ -3,32 +3,11 @@ package purchaseorder import ( - "flag" - "os" "testing" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/context/testlogging" - "github.com/centrifuge/go-centrifuge/storage" "github.com/stretchr/testify/assert" ) -func TestMain(m *testing.M) { - ibootstappers := []bootstrap.TestBootstrapper{ - &testlogging.TestLoggingBootstrapper{}, - &config.Bootstrapper{}, - &storage.Bootstrapper{}, - &Bootstrapper{}, - } - - bootstrap.RunTestBootstrappers(ibootstappers, nil) - flag.Parse() - result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) - os.Exit(result) -} - func TestBootstrapper_Bootstrap(t *testing.T) { err := (&Bootstrapper{}).Bootstrap(map[string]interface{}{}) assert.Error(t, err, "Should throw an error because of empty context") diff --git a/documents/purchaseorder/handler.go b/documents/purchaseorder/handler.go index abf348533..11e540977 100644 --- a/documents/purchaseorder/handler.go +++ b/documents/purchaseorder/handler.go @@ -6,7 +6,10 @@ import ( "github.com/centrifuge/centrifuge-protobufs/documenttypes" "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/code" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" clientpurchaseorderpb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" "github.com/ethereum/go-ethereum/common/hexutil" logging "github.com/ipfs/go-log" @@ -19,23 +22,26 @@ var apiLog = logging.Logger("purchaseorder-api") // anchoring, sending, finding stored purchase order document type grpcHandler struct { service Service + config config.Configuration } // GRPCHandler returns an implementation of the purchaseorder DocumentServiceServer -func GRPCHandler() (clientpurchaseorderpb.DocumentServiceServer, error) { - srv, err := documents.GetRegistryInstance().LocateService(documenttypes.PurchaseOrderDataTypeUrl) +func GRPCHandler(config config.Configuration, registry *documents.ServiceRegistry) (clientpurchaseorderpb.DocumentServiceServer, error) { + srv, err := registry.LocateService(documenttypes.PurchaseOrderDataTypeUrl) if err != nil { - return nil, fmt.Errorf("failed to fetch purchase order service") + return nil, errors.New("failed to fetch purchase order service") } return grpcHandler{ service: srv.(Service), + config: config, }, nil } // Create validates the purchase order, persists it to DB, and anchors it the chain func (h grpcHandler) Create(ctx context.Context, req *clientpurchaseorderpb.PurchaseOrderCreatePayload) (*clientpurchaseorderpb.PurchaseOrderResponse, error) { - ctxh, err := documents.NewContextHeader() + apiLog.Debugf("Create request %v", req) + ctxh, err := header.NewContextHeader(ctx, h.config) if err != nil { apiLog.Error(err) return nil, centerrors.New(code.Unknown, err.Error()) @@ -44,22 +50,29 @@ func (h grpcHandler) Create(ctx context.Context, req *clientpurchaseorderpb.Purc doc, err := h.service.DeriveFromCreatePayload(req, ctxh) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive create payload") } // validate, persist, and anchor - doc, err = h.service.Create(ctx, doc) + doc, err = h.service.Create(ctxh, doc) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not create document") } - return h.service.DerivePurchaseOrderResponse(doc) + resp, err := h.service.DerivePurchaseOrderResponse(doc) + if err != nil { + apiLog.Error(err) + return nil, centerrors.Wrap(err, "could not derive response") + } + + return resp, nil } // Update handles the document update and anchoring func (h grpcHandler) Update(ctx context.Context, payload *clientpurchaseorderpb.PurchaseOrderUpdatePayload) (*clientpurchaseorderpb.PurchaseOrderResponse, error) { - ctxHeader, err := documents.NewContextHeader() + apiLog.Debugf("Update request %v", payload) + ctxHeader, err := header.NewContextHeader(ctx, h.config) if err != nil { apiLog.Error(err) return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get header: %v", err)) @@ -68,59 +81,74 @@ func (h grpcHandler) Update(ctx context.Context, payload *clientpurchaseorderpb. doc, err := h.service.DeriveFromUpdatePayload(payload, ctxHeader) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive update payload") + } + + doc, err = h.service.Update(ctxHeader, doc) + if err != nil { + apiLog.Error(err) + return nil, centerrors.Wrap(err, "could not update document") } - doc, err = h.service.Update(ctx, doc) + resp, err := h.service.DerivePurchaseOrderResponse(doc) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive response") } - return h.service.DerivePurchaseOrderResponse(doc) + return resp, nil } // GetVersion returns the requested version of a purchase order func (h grpcHandler) GetVersion(ctx context.Context, req *clientpurchaseorderpb.GetVersionRequest) (*clientpurchaseorderpb.PurchaseOrderResponse, error) { + apiLog.Debugf("GetVersion request %v", req) identifier, err := hexutil.Decode(req.Identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "identifier is invalid") } + version, err := hexutil.Decode(req.Version) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "version is invalid") } + model, err := h.service.GetVersion(identifier, version) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "document not found") } + resp, err := h.service.DerivePurchaseOrderResponse(model) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive response") } + return resp, nil } // Get returns the purchase order the latest version of the document with given identifier func (h grpcHandler) Get(ctx context.Context, getRequest *clientpurchaseorderpb.GetRequest) (*clientpurchaseorderpb.PurchaseOrderResponse, error) { + apiLog.Debugf("Get request %v", getRequest) identifier, err := hexutil.Decode(getRequest.Identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "identifier is an invalid hex string") } + model, err := h.service.GetCurrentVersion(identifier) if err != nil { apiLog.Error(err) return nil, centerrors.Wrap(err, "document not found") } + resp, err := h.service.DerivePurchaseOrderResponse(model) if err != nil { apiLog.Error(err) - return nil, err + return nil, centerrors.Wrap(err, "could not derive response") } + return resp, nil } diff --git a/documents/purchaseorder/handler_test.go b/documents/purchaseorder/handler_test.go index 890ae1222..b81d677c1 100644 --- a/documents/purchaseorder/handler_test.go +++ b/documents/purchaseorder/handler_test.go @@ -4,11 +4,12 @@ package purchaseorder import ( "context" - "fmt" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" clientpopb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/ethereum/go-ethereum/common/hexutil" @@ -21,19 +22,19 @@ type mockService struct { mock.Mock } -func (m mockService) Create(ctx context.Context, doc documents.Model) (documents.Model, error) { +func (m mockService) Create(ctx *header.ContextHeader, doc documents.Model) (documents.Model, error) { args := m.Called(ctx, doc) model, _ := args.Get(0).(documents.Model) return model, args.Error(1) } -func (m mockService) Update(ctx context.Context, doc documents.Model) (documents.Model, error) { +func (m mockService) Update(ctx *header.ContextHeader, doc documents.Model) (documents.Model, error) { args := m.Called(ctx, doc) model, _ := args.Get(0).(documents.Model) return model, args.Error(1) } -func (m mockService) DeriveFromCreatePayload(req *clientpopb.PurchaseOrderCreatePayload, ctxh *documents.ContextHeader) (documents.Model, error) { +func (m mockService) DeriveFromCreatePayload(req *clientpopb.PurchaseOrderCreatePayload, ctxh *header.ContextHeader) (documents.Model, error) { args := m.Called(req, ctxh) model, _ := args.Get(0).(documents.Model) return model, args.Error(1) @@ -63,23 +64,23 @@ func (m mockService) DerivePurchaseOrderResponse(po documents.Model) (*clientpop return data, args.Error(1) } -func (m mockService) DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, ctxH *documents.ContextHeader) (documents.Model, error) { - args := m.Called(payload, ctxH) +func (m mockService) DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, ctxh *header.ContextHeader) (documents.Model, error) { + args := m.Called(payload, ctxh) doc, _ := args.Get(0).(documents.Model) return doc, args.Error(1) } func TestGRPCHandler_Create(t *testing.T) { - h := grpcHandler{} + h := getHandler() req := testingdocuments.CreatePOPayload() ctx := context.Background() model := &testingdocuments.MockModel{} - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(ctx, cfg) assert.Nil(t, err) // derive fails - srv := mockService{} - srv.On("DeriveFromCreatePayload", req, ctxh).Return(nil, fmt.Errorf("derive failed")).Once() + srv := h.service.(*mockService) + srv.On("DeriveFromCreatePayload", req, ctxh).Return(nil, errors.New("derive failed")).Once() h.service = srv resp, err := h.Create(ctx, req) srv.AssertExpectations(t) @@ -88,9 +89,8 @@ func TestGRPCHandler_Create(t *testing.T) { assert.Contains(t, err.Error(), "derive failed") // create fails - srv = mockService{} srv.On("DeriveFromCreatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Create", ctx, model).Return(nil, fmt.Errorf("create failed")).Once() + srv.On("Create", ctxh, model).Return(nil, errors.New("create failed")).Once() h.service = srv resp, err = h.Create(ctx, req) srv.AssertExpectations(t) @@ -99,10 +99,9 @@ func TestGRPCHandler_Create(t *testing.T) { assert.Contains(t, err.Error(), "create failed") // derive response fails - srv = mockService{} srv.On("DeriveFromCreatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Create", ctx, model).Return(model, nil).Once() - srv.On("DerivePurchaseOrderResponse", model).Return(nil, fmt.Errorf("derive response fails")).Once() + srv.On("Create", ctxh, model).Return(model, nil).Once() + srv.On("DerivePurchaseOrderResponse", model).Return(nil, errors.New("derive response fails")).Once() h.service = srv resp, err = h.Create(ctx, req) srv.AssertExpectations(t) @@ -112,9 +111,8 @@ func TestGRPCHandler_Create(t *testing.T) { // success eresp := &clientpopb.PurchaseOrderResponse{} - srv = mockService{} srv.On("DeriveFromCreatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Create", ctx, model).Return(model, nil).Once() + srv.On("Create", ctxh, model).Return(model, nil).Once() srv.On("DerivePurchaseOrderResponse", model).Return(eresp, nil).Once() h.service = srv resp, err = h.Create(ctx, req) @@ -125,7 +123,7 @@ func TestGRPCHandler_Create(t *testing.T) { } func TestGrpcHandler_Update(t *testing.T) { - h := grpcHandler{} + h := getHandler() p := testingdocuments.CreatePOPayload() req := &clientpopb.PurchaseOrderUpdatePayload{ Data: p.Data, @@ -133,12 +131,12 @@ func TestGrpcHandler_Update(t *testing.T) { } ctx := context.Background() model := &testingdocuments.MockModel{} - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(ctx, cfg) assert.Nil(t, err) // derive fails - srv := mockService{} - srv.On("DeriveFromUpdatePayload", req, ctxh).Return(nil, fmt.Errorf("derive failed")).Once() + srv := h.service.(*mockService) + srv.On("DeriveFromUpdatePayload", req, ctxh).Return(nil, errors.New("derive failed")).Once() h.service = srv resp, err := h.Update(ctx, req) srv.AssertExpectations(t) @@ -147,9 +145,8 @@ func TestGrpcHandler_Update(t *testing.T) { assert.Contains(t, err.Error(), "derive failed") // create fails - srv = mockService{} srv.On("DeriveFromUpdatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(nil, fmt.Errorf("update failed")).Once() + srv.On("Update", ctxh, model).Return(nil, errors.New("update failed")).Once() h.service = srv resp, err = h.Update(ctx, req) srv.AssertExpectations(t) @@ -158,10 +155,9 @@ func TestGrpcHandler_Update(t *testing.T) { assert.Contains(t, err.Error(), "update failed") // derive response fails - srv = mockService{} srv.On("DeriveFromUpdatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(model, nil).Once() - srv.On("DerivePurchaseOrderResponse", model).Return(nil, fmt.Errorf("derive response fails")).Once() + srv.On("Update", ctxh, model).Return(model, nil).Once() + srv.On("DerivePurchaseOrderResponse", model).Return(nil, errors.New("derive response fails")).Once() h.service = srv resp, err = h.Update(ctx, req) srv.AssertExpectations(t) @@ -171,9 +167,8 @@ func TestGrpcHandler_Update(t *testing.T) { // success eresp := &clientpopb.PurchaseOrderResponse{} - srv = mockService{} srv.On("DeriveFromUpdatePayload", req, ctxh).Return(model, nil).Once() - srv.On("Update", ctx, model).Return(model, nil).Once() + srv.On("Update", ctxh, model).Return(model, nil).Once() srv.On("DerivePurchaseOrderResponse", model).Return(eresp, nil).Once() h.service = srv resp, err = h.Update(ctx, req) @@ -190,7 +185,7 @@ type mockModel struct { } func getHandler() *grpcHandler { - return &grpcHandler{service: &mockService{}} + return &grpcHandler{service: &mockService{}, config: cfg} } func TestGrpcHandler_Get(t *testing.T) { @@ -225,7 +220,7 @@ func TestGrpcHandler_GetVersion_invalid_input(t *testing.T) { payload.Version = "0x00" payload.Identifier = "0x01" - mockErr := fmt.Errorf("not found") + mockErr := errors.New("not found") srv.On("GetVersion", []byte{0x01}, []byte{0x00}).Return(nil, mockErr) res, err = h.GetVersion(context.Background(), payload) srv.AssertExpectations(t) diff --git a/documents/purchaseorder/model.go b/documents/purchaseorder/model.go index 55ab77986..6d0977724 100644 --- a/documents/purchaseorder/model.go +++ b/documents/purchaseorder/model.go @@ -3,7 +3,6 @@ package purchaseorder import ( "crypto/sha256" "encoding/json" - "fmt" "reflect" "github.com/centrifuge/centrifuge-protobufs/documenttypes" @@ -11,7 +10,8 @@ import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/purchaseorder" "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" clientpurchaseorderpb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" "github.com/centrifuge/precise-proofs/proofs" @@ -22,48 +22,40 @@ import ( "github.com/golang/protobuf/ptypes/timestamp" ) +const prefix string = "po" + // PurchaseOrder implements the documents.Model keeps track of purchase order related fields and state type PurchaseOrder struct { - // purchase order number or reference number - PoNumber string - // name of the ordering company - OrderName string - // street and address details of the ordering company - OrderStreet string - OrderCity string - OrderZipcode string - // country ISO code of the ordering company of this purchase order - OrderCountry string - // name of the recipient company - RecipientName string - RecipientStreet string - RecipientCity string - RecipientZipcode string - // country ISO code of the receipient of this purchase order - RecipientCountry string - // ISO currency code - Currency string - // ordering gross amount including tax - OrderAmount int64 - // invoice amount excluding tax - NetAmount int64 - TaxAmount int64 - TaxRate int64 - Recipient *identity.CentID - Order []byte - // contact or requester or purchaser at the ordering company - OrderContact string - Comment string - // requested delivery date - DeliveryDate *timestamp.Timestamp - // purchase order date - DateCreated *timestamp.Timestamp - ExtraData []byte - + Status string // status of the Purchase Order + PoNumber string // purchase order number or reference number + OrderName string // name of the ordering company + OrderStreet string // street and address details of the ordering company + OrderCity string + OrderZipcode string + OrderCountry string // country ISO code of the ordering company of this purchase order + RecipientName string // name of the recipient company + RecipientStreet string + RecipientCity string + RecipientZipcode string + RecipientCountry string // country ISO code of the receipient of this purchase order + Currency string // ISO currency code + OrderAmount int64 // ordering gross amount including tax + NetAmount int64 // invoice amount excluding tax + TaxAmount int64 + TaxRate int64 + Recipient *identity.CentID + Order []byte + OrderContact string + Comment string + DeliveryDate *timestamp.Timestamp // requested delivery date + DateCreated *timestamp.Timestamp // purchase order date + ExtraData []byte PurchaseOrderSalt *purchaseorderpb.PurchaseOrderDataSalts CoreDocument *coredocumentpb.CoreDocument } +// ID returns the DocumentIdentifier for this document +// Note: this is not same as VersionIdentifier func (p *PurchaseOrder) ID() ([]byte, error) { coreDoc, err := p.PackCoreDocument() if err != nil { @@ -90,6 +82,7 @@ func (p *PurchaseOrder) getClientData() *clientpurchaseorderpb.PurchaseOrderData } return &clientpurchaseorderpb.PurchaseOrderData{ + PoStatus: p.Status, PoNumber: p.PoNumber, OrderName: p.OrderName, OrderStreet: p.OrderStreet, @@ -125,6 +118,7 @@ func (p *PurchaseOrder) createP2PProtobuf() *purchaseorderpb.PurchaseOrderData { } return &purchaseorderpb.PurchaseOrderData{ + PoStatus: p.Status, PoNumber: p.PoNumber, OrderName: p.OrderName, OrderStreet: p.OrderStreet, @@ -153,16 +147,16 @@ func (p *PurchaseOrder) createP2PProtobuf() *purchaseorderpb.PurchaseOrderData { } // InitPurchaseOrderInput initialize the model based on the received parameters from the rest api call -func (p *PurchaseOrder) InitPurchaseOrderInput(payload *clientpurchaseorderpb.PurchaseOrderCreatePayload, contextHeader *documents.ContextHeader) error { +func (p *PurchaseOrder) InitPurchaseOrderInput(payload *clientpurchaseorderpb.PurchaseOrderCreatePayload, contextHeader *header.ContextHeader) error { err := p.initPurchaseOrderFromData(payload.Data) if err != nil { return err } - collaborators := append([]string{contextHeader.Self().String()}, payload.Collaborators...) + collaborators := append([]string{contextHeader.Self().ID.String()}, payload.Collaborators...) p.CoreDocument, err = coredocument.NewWithCollaborators(collaborators) if err != nil { - return fmt.Errorf("failed to init core document: %v", err) + return errors.New("failed to init core document: %v", err) } return nil @@ -170,6 +164,7 @@ func (p *PurchaseOrder) InitPurchaseOrderInput(payload *clientpurchaseorderpb.Pu // initPurchaseOrderFromData initialises purchase order from purchaseOrderData func (p *PurchaseOrder) initPurchaseOrderFromData(data *clientpurchaseorderpb.PurchaseOrderData) error { + p.Status = data.PoStatus p.PoNumber = data.PoNumber p.OrderName = data.OrderName p.OrderStreet = data.OrderStreet @@ -218,31 +213,32 @@ func (p *PurchaseOrder) initPurchaseOrderFromData(data *clientpurchaseorderpb.Pu } // loadFromP2PProtobuf loads the purcase order from centrifuge protobuf purchase order data -func (p *PurchaseOrder) loadFromP2PProtobuf(purchaseOrderData *purchaseorderpb.PurchaseOrderData) { - p.PoNumber = purchaseOrderData.PoNumber - p.OrderName = purchaseOrderData.OrderName - p.OrderStreet = purchaseOrderData.OrderStreet - p.OrderCity = purchaseOrderData.OrderCity - p.OrderZipcode = purchaseOrderData.OrderZipcode - p.OrderCountry = purchaseOrderData.OrderCountry - p.RecipientName = purchaseOrderData.RecipientName - p.RecipientStreet = purchaseOrderData.RecipientStreet - p.RecipientCity = purchaseOrderData.RecipientCity - p.RecipientZipcode = purchaseOrderData.RecipientZipcode - p.RecipientCountry = purchaseOrderData.RecipientCountry - p.Currency = purchaseOrderData.Currency - p.OrderAmount = purchaseOrderData.OrderAmount - p.NetAmount = purchaseOrderData.NetAmount - p.TaxAmount = purchaseOrderData.TaxAmount - p.TaxRate = purchaseOrderData.TaxRate - p.Order = purchaseOrderData.Order - p.OrderContact = purchaseOrderData.OrderContact - p.Comment = purchaseOrderData.Comment - p.DeliveryDate = purchaseOrderData.DeliveryDate - p.DateCreated = purchaseOrderData.DateCreated - p.ExtraData = purchaseOrderData.ExtraData - - if recipient, err := identity.ToCentID(purchaseOrderData.Recipient); err == nil { +func (p *PurchaseOrder) loadFromP2PProtobuf(data *purchaseorderpb.PurchaseOrderData) { + p.Status = data.PoStatus + p.PoNumber = data.PoNumber + p.OrderName = data.OrderName + p.OrderStreet = data.OrderStreet + p.OrderCity = data.OrderCity + p.OrderZipcode = data.OrderZipcode + p.OrderCountry = data.OrderCountry + p.RecipientName = data.RecipientName + p.RecipientStreet = data.RecipientStreet + p.RecipientCity = data.RecipientCity + p.RecipientZipcode = data.RecipientZipcode + p.RecipientCountry = data.RecipientCountry + p.Currency = data.Currency + p.OrderAmount = data.OrderAmount + p.NetAmount = data.NetAmount + p.TaxAmount = data.TaxAmount + p.TaxRate = data.TaxRate + p.Order = data.Order + p.OrderContact = data.OrderContact + p.Comment = data.Comment + p.DeliveryDate = data.DeliveryDate + p.DateCreated = data.DateCreated + p.ExtraData = data.ExtraData + + if recipient, err := identity.ToCentID(data.Recipient); err == nil { p.Recipient = &recipient } } @@ -301,7 +297,7 @@ func (p *PurchaseOrder) UnpackCoreDocument(coreDoc *coredocumentpb.CoreDocument) coreDoc.EmbeddedData.TypeUrl != documenttypes.PurchaseOrderDataTypeUrl || coreDoc.EmbeddedDataSalts == nil || coreDoc.EmbeddedDataSalts.TypeUrl != documenttypes.PurchaseOrderSaltsTypeUrl { - return fmt.Errorf("trying to convert document with incorrect schema") + return errors.New("trying to convert document with incorrect schema") } poData := &purchaseorderpb.PurchaseOrderData{} @@ -345,7 +341,7 @@ func (p *PurchaseOrder) Type() reflect.Type { func (p *PurchaseOrder) calculateDataRoot() error { t, err := p.getDocumentDataTree() if err != nil { - return fmt.Errorf("calculateDataRoot error %v", err) + return errors.New("calculateDataRoot error %v", err) } p.CoreDocument.DataRoot = t.RootHash() return nil @@ -353,15 +349,16 @@ func (p *PurchaseOrder) calculateDataRoot() error { // getDocumentDataTree creates precise-proofs data tree for the model func (p *PurchaseOrder) getDocumentDataTree() (tree *proofs.DocumentTree, err error) { - t := proofs.NewDocumentTree(proofs.TreeOptions{EnableHashSorting: true, Hash: sha256.New()}) + prop := proofs.NewProperty(prefix) + t := proofs.NewDocumentTree(proofs.TreeOptions{EnableHashSorting: true, Hash: sha256.New(), ParentPrefix: &prop}) poData := p.createP2PProtobuf() err = t.AddLeavesFromDocument(poData, p.getPurchaseOrderSalts(poData)) if err != nil { - return nil, fmt.Errorf("getDocumentDataTree error %v", err) + return nil, errors.New("getDocumentDataTree error %v", err) } err = t.Generate() if err != nil { - return nil, fmt.Errorf("getDocumentDataTree error %v", err) + return nil, errors.New("getDocumentDataTree error %v", err) } return &t, nil } @@ -372,14 +369,14 @@ func (p *PurchaseOrder) createProofs(fields []string) (coreDoc *coredocumentpb.C // is still not saved with roots in db due to failures during getting signatures. coreDoc, err = p.PackCoreDocument() if err != nil { - return nil, nil, fmt.Errorf("createProofs error %v", err) + return nil, nil, errors.New("createProofs error %v", err) } tree, err := p.getDocumentDataTree() if err != nil { - return coreDoc, nil, fmt.Errorf("createProofs error %v", err) + return coreDoc, nil, errors.New("createProofs error %v", err) } proofs, err = coredocument.CreateProofs(tree, coreDoc, fields) - return + return coreDoc, proofs, err } diff --git a/documents/purchaseorder/model_test.go b/documents/purchaseorder/model_test.go index 9b10513c6..450b2098d 100644 --- a/documents/purchaseorder/model_test.go +++ b/documents/purchaseorder/model_test.go @@ -3,16 +3,28 @@ package purchaseorder import ( + "context" "encoding/json" + "os" "reflect" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/purchaseorder" + "github.com/centrifuge/go-centrifuge/anchors" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/context/testlogging" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/p2p" clientpurchaseorderpb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" + "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" @@ -20,6 +32,32 @@ import ( "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + +func TestMain(m *testing.M) { + ethClient := &testingcommons.MockEthClient{} + ethClient.On("GetEthClient").Return(nil) + ctx[ethereum.BootstrappedEthereumClient] = ethClient + + ibootstappers := []bootstrap.TestBootstrapper{ + &testlogging.TestLoggingBootstrapper{}, + &config.Bootstrapper{}, + &storage.Bootstrapper{}, + &queue.Bootstrapper{}, + &identity.Bootstrapper{}, + anchors.Bootstrapper{}, + documents.Bootstrapper{}, + p2p.Bootstrapper{}, + &Bootstrapper{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} + func TestPO_FromCoreDocuments_invalidParameter(t *testing.T) { poModel := &PurchaseOrder{} @@ -140,7 +178,7 @@ func TestPOModel_getClientData(t *testing.T) { } func TestPOOrderModel_InitPOInput(t *testing.T) { - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // fail recipient data := &clientpurchaseorderpb.PurchaseOrderData{ @@ -173,12 +211,12 @@ func TestPOOrderModel_InitPOInput(t *testing.T) { assert.Equal(t, poModel.Recipient[:], []byte{1, 2, 3, 4, 5, 6}) assert.Equal(t, poModel.ExtraData[:], []byte{1, 2, 3, 2, 3, 1}) - id := contextHeader.Self() + id := contextHeader.Self().ID assert.Equal(t, poModel.CoreDocument.Collaborators, [][]byte{id[:], {1, 1, 2, 4, 5, 6}, {1, 2, 3, 2, 3, 2}}) } func TestPOModel_calculateDataRoot(t *testing.T) { - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) poModel := new(PurchaseOrder) err = poModel.InitPurchaseOrderInput(testingdocuments.CreatePOPayload(), contextHeader) @@ -194,7 +232,7 @@ func TestPOModel_calculateDataRoot(t *testing.T) { func TestPOModel_createProofs(t *testing.T) { poModel, corDoc, err := createMockPurchaseOrder(t) assert.Nil(t, err) - corDoc, proof, err := poModel.createProofs([]string{"po_number", "collaborators[0]", "document_type"}) + corDoc, proof, err := poModel.createProofs([]string{"po.po_number", "collaborators[0]", "document_type"}) assert.Nil(t, err) assert.NotNil(t, proof) assert.NotNil(t, corDoc) @@ -210,6 +248,9 @@ func TestPOModel_createProofs(t *testing.T) { assert.Nil(t, err) assert.True(t, valid) + // Validate '0x' Hex format in []byte value + assert.Equal(t, hexutil.Encode(poModel.CoreDocument.Collaborators[0]), proof[1].Value) + // Validate document_type valid, err = tree.ValidateProof(proof[2]) assert.Nil(t, err) @@ -227,8 +268,9 @@ func TestPOModel_getDocumentDataTree(t *testing.T) { poModel := PurchaseOrder{PoNumber: "3213121", NetAmount: 2, OrderAmount: 2} tree, err := poModel.getDocumentDataTree() assert.Nil(t, err, "tree should be generated without error") - _, leaf := tree.GetLeafByProperty("po_number") - assert.Equal(t, "po_number", leaf.Property) + _, leaf := tree.GetLeafByProperty("po.po_number") + assert.NotNil(t, leaf) + assert.Equal(t, "po.po_number", leaf.Property.ReadableName()) } func createMockPurchaseOrder(t *testing.T) (*PurchaseOrder, *coredocumentpb.CoreDocument, error) { diff --git a/documents/purchaseorder/repository.go b/documents/purchaseorder/repository.go deleted file mode 100644 index 079e04f03..000000000 --- a/documents/purchaseorder/repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package purchaseorder - -import ( - "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/storage" -) - -// repository is the purchase order repository -type repository struct { - documents.LevelDBRepository -} - -// getRepository returns the implemented documents.legacyRepo for purchase orders -func getRepository() documents.Repository { - return &repository{ - documents.LevelDBRepository{ - KeyPrefix: "purchaseorder", - LevelDB: storage.GetLevelDBStorage(), - }, - } -} diff --git a/documents/purchaseorder/repository_test.go b/documents/purchaseorder/repository_test.go deleted file mode 100644 index f2e4d62bb..000000000 --- a/documents/purchaseorder/repository_test.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build unit - -package purchaseorder - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRepository_getRepository(t *testing.T) { - r := getRepository() - assert.NotNil(t, r) - assert.Equal(t, "purchaseorder", r.(*repository).KeyPrefix) -} diff --git a/documents/purchaseorder/service.go b/documents/purchaseorder/service.go index a6ac17bb6..b6a8cf4fc 100644 --- a/documents/purchaseorder/service.go +++ b/documents/purchaseorder/service.go @@ -2,24 +2,22 @@ package purchaseorder import ( "bytes" - "context" - "fmt" "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/notification" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/centerrors" - "github.com/centrifuge/go-centrifuge/code" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/coredocument/processor" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" - centED25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/centrifuge/go-centrifuge/notification" clientpopb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" "github.com/centrifuge/go-centrifuge/signatures" + "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/ptypes" logging "github.com/ipfs/go-log" @@ -32,16 +30,16 @@ type Service interface { documents.Service // DeriverFromPayload derives purchase order from clientPayload - DeriveFromCreatePayload(payload *clientpopb.PurchaseOrderCreatePayload, hdr *documents.ContextHeader) (documents.Model, error) + DeriveFromCreatePayload(payload *clientpopb.PurchaseOrderCreatePayload, hdr *header.ContextHeader) (documents.Model, error) // DeriveFromUpdatePayload derives purchase order from update payload - DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, hdr *documents.ContextHeader) (documents.Model, error) + DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, hdr *header.ContextHeader) (documents.Model, error) // Create validates and persists purchase order and returns a Updated model - Create(ctx context.Context, po documents.Model) (documents.Model, error) + Create(ctx *header.ContextHeader, po documents.Model) (documents.Model, error) // Update validates and updates the purchase order and return the updated model - Update(ctx context.Context, po documents.Model) (documents.Model, error) + Update(ctx *header.ContextHeader, po documents.Model) (documents.Model, error) // DerivePurchaseOrderData returns the purchase order data as client data DerivePurchaseOrderData(po documents.Model) (*clientpopb.PurchaseOrderData, error) @@ -51,17 +49,19 @@ type Service interface { } // service implements Service and handles all purchase order related persistence and validations -// service always returns errors of type `centerrors` with proper error code +// service always returns errors of type `errors.Error` or `errors.TypedError` type service struct { + config documents.Config repo documents.Repository - coreDocProcessor coredocumentprocessor.Processor + coreDocProcessor coredocument.Processor notifier notification.Sender anchorRepository anchors.AnchorRepository + identityService identity.Service } // DefaultService returns the default implementation of the service -func DefaultService(repo documents.Repository, processor coredocumentprocessor.Processor, anchorRepository anchors.AnchorRepository) Service { - return service{repo: repo, coreDocProcessor: processor, notifier: ¬ification.WebhookSender{}, anchorRepository: anchorRepository} +func DefaultService(config config.Configuration, repo documents.Repository, processor coredocument.Processor, anchorRepository anchors.AnchorRepository, identityService identity.Service) Service { + return service{config: config, repo: repo, coreDocProcessor: processor, notifier: notification.NewWebhookSender(config), anchorRepository: anchorRepository, identityService: identityService} } // DeriveFromCoreDocument takes a core document and returns a purchase order @@ -69,7 +69,7 @@ func (s service) DeriveFromCoreDocument(cd *coredocumentpb.CoreDocument) (docume var model documents.Model = new(PurchaseOrder) err := model.UnpackCoreDocument(cd) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentUnPackingCoreDocument, err) } return model, nil @@ -79,55 +79,71 @@ func (s service) DeriveFromCoreDocument(cd *coredocumentpb.CoreDocument) (docume func (s service) calculateDataRoot(old, new documents.Model, validator documents.Validator) (documents.Model, error) { po, ok := new.(*PurchaseOrder) if !ok { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("unknown document type: %T", new)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalidType, errors.New("unknown document type: %T", new)) } // create data root, has to be done at the model level to access fields err := po.calculateDataRoot() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } - // validate the purchase order + // validate the invoice err = validator.Validate(old, po) if err != nil { - return nil, centerrors.NewWithErrors(code.DocumentInvalid, "validations failed", documents.ConvertToMap(err)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) + } + + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) } // we use CurrentVersion as the id since that will be unique across multiple versions of the same document - err = s.repo.Create(po.CoreDocument.CurrentVersion, po) + err = s.repo.Create(tenantID, po.CoreDocument.CurrentVersion, po) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) } return po, nil } // Create validates, persists, and anchors a purchase order -func (s service) Create(ctx context.Context, po documents.Model) (documents.Model, error) { +func (s service) Create(ctx *header.ContextHeader, po documents.Model) (documents.Model, error) { po, err := s.calculateDataRoot(nil, po, CreateValidator()) if err != nil { return nil, err } - po, err = documents.AnchorDocument(ctx, po, s.coreDocProcessor, s.repo.Update) + po, err = documents.AnchorDocument(ctx, po, s.coreDocProcessor, s.updater) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, err } return po, nil } +// updater wraps logic related to updating documents so that it can be executed as a closure +func (s service) updater(id []byte, model documents.Model) error { + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + return s.repo.Update(tenantID, id, model) +} + // Update validates, persists, and anchors a new version of purchase order -func (s service) Update(ctx context.Context, po documents.Model) (documents.Model, error) { +func (s service) Update(ctx *header.ContextHeader, po documents.Model) (documents.Model, error) { cd, err := po.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } old, err := s.GetCurrentVersion(cd.DocumentIdentifier) if err != nil { - return nil, centerrors.New(code.DocumentNotFound, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentNotFound, err) } po, err = s.calculateDataRoot(old, po, UpdateValidator()) @@ -135,63 +151,63 @@ func (s service) Update(ctx context.Context, po documents.Model) (documents.Mode return nil, err } - po, err = documents.AnchorDocument(ctx, po, s.coreDocProcessor, s.repo.Update) + po, err = documents.AnchorDocument(ctx, po, s.coreDocProcessor, s.updater) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, err } return po, nil } // DeriveFromCreatePayload derives purchase order from create payload -func (s service) DeriveFromCreatePayload(payload *clientpopb.PurchaseOrderCreatePayload, ctxH *documents.ContextHeader) (documents.Model, error) { - if payload == nil { - return nil, centerrors.New(code.DocumentInvalid, "input is nil") +func (s service) DeriveFromCreatePayload(payload *clientpopb.PurchaseOrderCreatePayload, ctxH *header.ContextHeader) (documents.Model, error) { + if payload == nil || payload.Data == nil { + return nil, documents.ErrDocumentNil } po := new(PurchaseOrder) err := po.InitPurchaseOrderInput(payload, ctxH) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("purchase order init failed: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } return po, nil } // DeriveFromUpdatePayload derives purchase order from update payload -func (s service) DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, ctxH *documents.ContextHeader) (documents.Model, error) { - if payload == nil { - return nil, centerrors.New(code.DocumentInvalid, "invalid payload") +func (s service) DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdatePayload, ctxH *header.ContextHeader) (documents.Model, error) { + if payload == nil || payload.Data == nil { + return nil, documents.ErrDocumentNil } // get latest old version of the document id, err := hexutil.Decode(payload.Identifier) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to decode identifier: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentIdentifier, errors.New("failed to decode identifier: %v", err)) } old, err := s.GetCurrentVersion(id) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to fetch old version: %v", err)) + return nil, err } // load purchase order data po := new(PurchaseOrder) err = po.initPurchaseOrderFromData(payload.Data) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to load purchase order from data: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, errors.New("failed to load purchase order from data: %v", err)) } // update core document oldCD, err := old.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } - collaborators := append([]string{ctxH.Self().String()}, payload.Collaborators...) + collaborators := append([]string{ctxH.Self().ID.String()}, payload.Collaborators...) po.CoreDocument, err = coredocument.PrepareNewVersion(*oldCD, collaborators) if err != nil { - return nil, centerrors.New(code.DocumentInvalid, fmt.Sprintf("failed to prepare new version: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentPrepareCoreDocument, err) } return po, nil @@ -201,7 +217,7 @@ func (s service) DeriveFromUpdatePayload(payload *clientpopb.PurchaseOrderUpdate func (s service) DerivePurchaseOrderData(doc documents.Model) (*clientpopb.PurchaseOrderData, error) { po, ok := doc.(*PurchaseOrder) if !ok { - return nil, centerrors.New(code.DocumentInvalid, "document of invalid type") + return nil, documents.ErrDocumentInvalidType } return po.getClientData(), nil @@ -211,19 +227,19 @@ func (s service) DerivePurchaseOrderData(doc documents.Model) (*clientpopb.Purch func (s service) DerivePurchaseOrderResponse(doc documents.Model) (*clientpopb.PurchaseOrderResponse, error) { cd, err := doc.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } collaborators := make([]string, len(cd.Collaborators)) for i, c := range cd.Collaborators { cid, err := identity.ToCentID(c) if err != nil { - return nil, centerrors.New(code.Unknown, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentCollaborator, err) } collaborators[i] = cid.String() } - header := &clientpopb.ResponseHeader{ + h := &clientpopb.ResponseHeader{ DocumentId: hexutil.Encode(cd.DocumentIdentifier), VersionId: hexutil.Encode(cd.CurrentVersion), Collaborators: collaborators, @@ -235,24 +251,28 @@ func (s service) DerivePurchaseOrderResponse(doc documents.Model) (*clientpopb.P } return &clientpopb.PurchaseOrderResponse{ - Header: header, + Header: h, Data: data, }, nil } func (s service) getPurchaseOrderVersion(documentID, version []byte) (model *PurchaseOrder, err error) { - var doc documents.Model = new(PurchaseOrder) - err = s.repo.LoadByID(version, doc) + // get tenant ID + tenantID, err := s.config.GetIdentityID() if err != nil { - return nil, err + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + doc, err := s.repo.Get(tenantID, version) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentVersionNotFound, err) } model, ok := doc.(*PurchaseOrder) if !ok { - return nil, err + return nil, documents.ErrDocumentInvalidType } if !bytes.Equal(model.CoreDocument.DocumentIdentifier, documentID) { - return nil, centerrors.New(code.DocumentInvalid, "version is not valid for this identifier") + return nil, errors.NewTypedError(documents.ErrDocumentVersionNotFound, errors.New("version is not valid for this identifier")) } return model, nil } @@ -261,17 +281,18 @@ func (s service) getPurchaseOrderVersion(documentID, version []byte) (model *Pur func (s service) GetCurrentVersion(documentID []byte) (documents.Model, error) { model, err := s.getPurchaseOrderVersion(documentID, documentID) if err != nil { - return nil, centerrors.Wrap(err, "document not found") + return nil, errors.NewTypedError(documents.ErrDocumentNotFound, err) } nextVersion := model.CoreDocument.NextVersion for nextVersion != nil { temp, err := s.getPurchaseOrderVersion(documentID, nextVersion) if err != nil { + // here the err is returned as nil because it is expected that the nextVersion is not available in the db at some stage of the iteration return model, nil - } else { - model = temp - nextVersion = model.CoreDocument.NextVersion } + + model = temp + nextVersion = model.CoreDocument.NextVersion } return model, nil } @@ -280,7 +301,7 @@ func (s service) GetCurrentVersion(documentID []byte) (documents.Model, error) { func (s service) GetVersion(documentID []byte, version []byte) (documents.Model, error) { po, err := s.getPurchaseOrderVersion(documentID, version) if err != nil { - return nil, centerrors.Wrap(err, "document not found for the given version") + return nil, err } return po, nil @@ -290,18 +311,18 @@ func (s service) GetVersion(documentID []byte, version []byte) (documents.Model, func (s service) purchaseOrderProof(model documents.Model, fields []string) (*documents.DocumentProof, error) { po, ok := model.(*PurchaseOrder) if !ok { - return nil, centerrors.New(code.DocumentInvalid, "document of invalid type") + return nil, documents.ErrDocumentInvalidType } - if err := coredocument.PostAnchoredValidator(s.anchorRepository).Validate(nil, po); err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + if err := coredocument.PostAnchoredValidator(s.identityService, s.anchorRepository).Validate(nil, po); err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } coreDoc, proofs, err := po.createProofs(fields) if err != nil { - return nil, err + return nil, errors.NewTypedError(documents.ErrDocumentProof, err) } return &documents.DocumentProof{ - DocumentId: coreDoc.DocumentIdentifier, - VersionId: coreDoc.CurrentVersion, + DocumentID: coreDoc.DocumentIdentifier, + VersionID: coreDoc.CurrentVersion, FieldProofs: proofs, }, nil } @@ -327,33 +348,46 @@ func (s service) CreateProofsForVersion(documentID, version []byte, fields []str // RequestDocumentSignature validates the document and returns the signature // Note: this is document agnostic. But since we do not have a common implementation, adding it here. // will remove this once we have a common implementation for documents.Service -func (s service) RequestDocumentSignature(model documents.Model) (*coredocumentpb.Signature, error) { - if err := coredocument.SignatureRequestValidator().Validate(nil, model); err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) +func (s service) RequestDocumentSignature(contextHeader *header.ContextHeader, model documents.Model) (*coredocumentpb.Signature, error) { + if err := coredocument.SignatureRequestValidator(s.identityService).Validate(nil, model); err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentInvalid, err) } cd, err := model.PackCoreDocument() if err != nil { - return nil, centerrors.New(code.DocumentInvalid, err.Error()) + return nil, errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) } srvLog.Infof("coredoc received %x with signing root %x", cd.DocumentIdentifier, cd.SigningRoot) - idConfig, err := centED25519.GetIDConfig() - if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get ID Config: %v", err)) + idKeys, ok := contextHeader.Self().Keys[identity.KeyPurposeSigning] + if !ok { + return nil, errors.NewTypedError(documents.ErrDocumentSigning, errors.New("missing signing key")) } - - sig := signatures.Sign(idConfig, cd.SigningRoot) + sig := signatures.Sign(contextHeader.Self().ID[:], idKeys.PrivateKey, idKeys.PublicKey, cd.SigningRoot) cd.Signatures = append(cd.Signatures, sig) err = model.UnpackCoreDocument(cd) if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to Unpack CoreDocument: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentUnPackingCoreDocument, err) } - err = s.repo.Create(cd.CurrentVersion, model) + // get tenant ID + tenantID, err := s.config.GetIdentityID() if err != nil { - return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to store document: %v", err)) + return nil, errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) + } + + // Logic for receiving version n (n > 1) of the document for the first time + if !s.repo.Exists(tenantID, cd.DocumentIdentifier) && !utils.IsSameByteSlice(cd.DocumentIdentifier, cd.CurrentVersion) { + err = s.repo.Create(tenantID, cd.DocumentIdentifier, model) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) + } + } + + err = s.repo.Create(tenantID, cd.CurrentVersion, model) + if err != nil { + return nil, errors.NewTypedError(documents.ErrDocumentPersistence, err) } srvLog.Infof("signed coredoc %x with version %x", cd.DocumentIdentifier, cd.CurrentVersion) @@ -364,27 +398,33 @@ func (s service) RequestDocumentSignature(model documents.Model) (*coredocumentp // Note: this is document agnostic. But since we do not have a common implementation, adding it here. // will remove this once we have a common implementation for documents.Service func (s service) ReceiveAnchoredDocument(model documents.Model, headers *p2ppb.CentrifugeHeader) error { - if err := coredocument.PostAnchoredValidator(s.anchorRepository).Validate(nil, model); err != nil { - return centerrors.New(code.DocumentInvalid, err.Error()) + if err := coredocument.PostAnchoredValidator(s.identityService, s.anchorRepository).Validate(nil, model); err != nil { + return errors.NewTypedError(documents.ErrDocumentInvalid, err) } doc, err := model.PackCoreDocument() if err != nil { - return centerrors.New(code.DocumentInvalid, err.Error()) + return errors.NewTypedError(documents.ErrDocumentPackingCoreDocument, err) + } + + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return errors.NewTypedError(documents.ErrDocumentConfigTenantID, err) } - err = s.repo.Update(doc.CurrentVersion, model) + err = s.repo.Update(tenantID, doc.CurrentVersion, model) if err != nil { - return centerrors.New(code.Unknown, err.Error()) + return errors.NewTypedError(documents.ErrDocumentPersistence, err) } ts, _ := ptypes.TimestampProto(time.Now().UTC()) notificationMsg := ¬ificationpb.NotificationMessage{ - EventType: uint32(notification.ReceivedPayload), - CentrifugeId: headers.SenderCentrifugeId, - Recorded: ts, - DocumentType: doc.EmbeddedData.TypeUrl, - DocumentIdentifier: doc.DocumentIdentifier, + EventType: uint32(notification.ReceivedPayload), + CentrifugeId: hexutil.Encode(headers.SenderCentrifugeId), + Recorded: ts, + DocumentType: doc.EmbeddedData.TypeUrl, + DocumentId: hexutil.Encode(doc.DocumentIdentifier), } // Async until we add queuing @@ -392,3 +432,13 @@ func (s service) ReceiveAnchoredDocument(model documents.Model, headers *p2ppb.C return nil } + +// Exists checks if an purchase order exists +func (s service) Exists(documentID []byte) bool { + // get tenant ID + tenantID, err := s.config.GetIdentityID() + if err != nil { + return false + } + return s.repo.Exists(tenantID, documentID) +} diff --git a/documents/purchaseorder/service_test.go b/documents/purchaseorder/service_test.go index 6cd20f594..f561bcc88 100644 --- a/documents/purchaseorder/service_test.go +++ b/documents/purchaseorder/service_test.go @@ -4,20 +4,21 @@ package purchaseorder import ( "context" - "fmt" "math/big" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/anchors" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" clientpurchaseorderpb "github.com/centrifuge/go-centrifuge/protobufs/gen/go/purchaseorder" - "github.com/centrifuge/go-centrifuge/signatures" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/storage" "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common/hexutil" @@ -26,9 +27,9 @@ import ( ) var ( - centID = utils.RandomSlice(identity.CentIDLength) - 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} + centIDBytes = utils.RandomSlice(identity.CentIDLength) + 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} ) type mockAnchorRepo struct { @@ -36,26 +37,31 @@ type mockAnchorRepo struct { anchors.AnchorRepository } -func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocRoot, error) { +func (r *mockAnchorRepo) GetDocumentRootOf(anchorID anchors.AnchorID) (anchors.DocumentRoot, error) { args := r.Called(anchorID) - docRoot, _ := args.Get(0).(anchors.DocRoot) + docRoot, _ := args.Get(0).(anchors.DocumentRoot) return docRoot, args.Error(1) } -func getServiceWithMockedLayers() Service { - return DefaultService(getRepository(), &testingutils.MockCoreDocumentProcessor{}, &mockAnchorRepo{}) +func getServiceWithMockedLayers() (*testingcommons.MockIDService, Service) { + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + idService := &testingcommons.MockIDService{} + idService.On("ValidateSignature", mock.Anything, mock.Anything).Return(nil) + return idService, DefaultService(c, testRepo(), &testingcoredocument.MockCoreDocumentProcessor{}, &mockAnchorRepo{}, idService) } func TestService_Update(t *testing.T) { - poSrv := service{repo: getRepository()} - ctx := context.Background() - ctxh, err := documents.NewContextHeader() + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // pack failed model := &testingdocuments.MockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("pack error")).Once() - _, err = poSrv.Update(ctx, model) + model.On("PackCoreDocument").Return(nil, errors.New("pack error")).Once() + _, err = poSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "pack error") @@ -64,7 +70,7 @@ func TestService_Update(t *testing.T) { model = &testingdocuments.MockModel{} cd := coredocument.New() model.On("PackCoreDocument").Return(cd, nil).Once() - _, err = poSrv.Update(ctx, model) + _, err = poSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "document not found") @@ -77,12 +83,12 @@ func TestService_Update(t *testing.T) { assert.Nil(t, err) cd.DocumentRoot = utils.RandomSlice(32) po.(*PurchaseOrder).CoreDocument = cd - getRepository().Create(cd.CurrentVersion, po) + testRepo().Create(centIDBytes, cd.CurrentVersion, po) // calculate data root fails model = &testingdocuments.MockModel{} model.On("PackCoreDocument").Return(cd, nil).Once() - _, err = poSrv.Update(ctx, model) + _, err = poSrv.Update(ctxh, model) model.AssertExpectations(t) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown document type") @@ -102,23 +108,23 @@ func TestService_Update(t *testing.T) { newData, err := poSrv.DerivePurchaseOrderData(newInv) assert.Nil(t, err) assert.Equal(t, data, newData) - proc := &testingutils.MockCoreDocumentProcessor{} + proc := &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", newInv).Return(nil).Once() - proc.On("RequestSignatures", ctx, newInv).Return(nil).Once() + proc.On("RequestSignatures", ctxh, newInv).Return(nil).Once() proc.On("PrepareForAnchoring", newInv).Return(nil).Once() proc.On("AnchorDocument", newInv).Return(nil).Once() - proc.On("SendDocument", ctx, newInv).Return(nil).Once() + proc.On("SendDocument", ctxh, newInv).Return(nil).Once() poSrv.coreDocProcessor = proc - po, err = poSrv.Update(ctx, newInv) + po, err = poSrv.Update(ctxh, newInv) proc.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, po) newCD, err := po.PackCoreDocument() assert.Nil(t, err) - assert.True(t, getRepository().Exists(newCD.DocumentIdentifier)) - assert.True(t, getRepository().Exists(newCD.CurrentVersion)) - assert.True(t, getRepository().Exists(newCD.PreviousVersion)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.DocumentIdentifier)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.CurrentVersion)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.PreviousVersion)) newData, err = poSrv.DerivePurchaseOrderData(po) assert.Nil(t, err) @@ -126,18 +132,26 @@ func TestService_Update(t *testing.T) { } func TestService_DeriveFromUpdatePayload(t *testing.T) { - poSrv := service{repo: getRepository()} + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} // nil payload doc, err := poSrv.DeriveFromUpdatePayload(nil, nil) assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid payload") + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) + assert.Nil(t, doc) + + // nil payload data + doc, err = poSrv.DeriveFromUpdatePayload(&clientpurchaseorderpb.PurchaseOrderUpdatePayload{}, nil) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) assert.Nil(t, doc) // messed up identifier - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) - payload := &clientpurchaseorderpb.PurchaseOrderUpdatePayload{Identifier: "some identifier"} + payload := &clientpurchaseorderpb.PurchaseOrderUpdatePayload{Identifier: "some identifier", Data: &clientpurchaseorderpb.PurchaseOrderData{}} doc, err = poSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to decode identifier") @@ -148,7 +162,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { payload.Identifier = hexutil.Encode(id) doc, err = poSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to fetch old version") + assert.True(t, errors.IsOfType(documents.ErrDocumentNotFound, err)) assert.Nil(t, doc) // failed to load from data @@ -158,7 +172,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { old.CoreDocument.DocumentIdentifier = id old.CoreDocument.CurrentVersion = id old.CoreDocument.DocumentRoot = utils.RandomSlice(32) - err = getRepository().Create(id, old) + err = testRepo().Create(centIDBytes, id, old) assert.Nil(t, err) payload.Data = &clientpurchaseorderpb.PurchaseOrderData{ Recipient: "0x010203040506", @@ -176,7 +190,7 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { payload.Collaborators = []string{"some wrong ID"} doc, err = poSrv.DeriveFromUpdatePayload(payload, contextHeader) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to prepare new version") + assert.True(t, errors.IsOfType(documents.ErrDocumentPrepareCoreDocument, err)) assert.Nil(t, doc) // success @@ -201,14 +215,20 @@ func TestService_DeriveFromUpdatePayload(t *testing.T) { func TestService_DeriveFromCreatePayload(t *testing.T) { poSrv := service{} - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // nil payload m, err := poSrv.DeriveFromCreatePayload(nil, ctxh) assert.Nil(t, m) assert.Error(t, err) - assert.Contains(t, err.Error(), "input is nil") + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) + + // nil data payload + m, err = poSrv.DeriveFromCreatePayload(&clientpurchaseorderpb.PurchaseOrderCreatePayload{}, ctxh) + assert.Nil(t, m) + assert.Error(t, err) + assert.True(t, errors.IsOfType(documents.ErrDocumentNil, err)) // Init fails payload := &clientpurchaseorderpb.PurchaseOrderCreatePayload{ @@ -220,7 +240,7 @@ func TestService_DeriveFromCreatePayload(t *testing.T) { m, err = poSrv.DeriveFromCreatePayload(payload, ctxh) assert.Nil(t, m) assert.Error(t, err) - assert.Contains(t, err.Error(), "purchase order init failed") + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalid, err)) // success payload.Data.ExtraData = "0x01020304050607" @@ -233,7 +253,7 @@ func TestService_DeriveFromCreatePayload(t *testing.T) { func TestService_DeriveFromCoreDocument(t *testing.T) { // nil doc - poSrv := service{repo: getRepository()} + poSrv := service{repo: testRepo()} _, err := poSrv.DeriveFromCoreDocument(nil) assert.Error(t, err, "must fail to derive") @@ -250,13 +270,14 @@ func TestService_DeriveFromCoreDocument(t *testing.T) { } func TestService_Create(t *testing.T) { - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) - poSrv := service{repo: getRepository()} - ctx := context.Background() + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} // calculate data root fails - m, err := poSrv.Create(context.Background(), &testingdocuments.MockModel{}) + m, err := poSrv.Create(ctxh, &testingdocuments.MockModel{}) assert.Nil(t, m) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown document type") @@ -264,10 +285,10 @@ func TestService_Create(t *testing.T) { // anchor fails po, err := poSrv.DeriveFromCreatePayload(testingdocuments.CreatePOPayload(), ctxh) assert.Nil(t, err) - proc := &testingutils.MockCoreDocumentProcessor{} - proc.On("PrepareForSignatureRequests", po).Return(fmt.Errorf("anchoring failed")).Once() + proc := &testingcoredocument.MockCoreDocumentProcessor{} + proc.On("PrepareForSignatureRequests", po).Return(errors.New("anchoring failed")).Once() poSrv.coreDocProcessor = proc - m, err = poSrv.Create(ctx, po) + m, err = poSrv.Create(ctxh, po) proc.AssertExpectations(t) assert.Nil(t, m) assert.Error(t, err) @@ -276,26 +297,22 @@ func TestService_Create(t *testing.T) { // success po, err = poSrv.DeriveFromCreatePayload(testingdocuments.CreatePOPayload(), ctxh) assert.Nil(t, err) - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", po).Return(nil).Once() - proc.On("RequestSignatures", ctx, po).Return(nil).Once() + proc.On("RequestSignatures", ctxh, po).Return(nil).Once() proc.On("PrepareForAnchoring", po).Return(nil).Once() proc.On("AnchorDocument", po).Return(nil).Once() - proc.On("SendDocument", ctx, po).Return(nil).Once() + proc.On("SendDocument", ctxh, po).Return(nil).Once() poSrv.coreDocProcessor = proc - m, err = poSrv.Create(ctx, po) + m, err = poSrv.Create(ctxh, po) proc.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, m) newCD, err := m.PackCoreDocument() assert.Nil(t, err) - assert.True(t, getRepository().Exists(newCD.DocumentIdentifier)) - assert.True(t, getRepository().Exists(newCD.CurrentVersion)) -} - -func setIdentityService(idService identity.Service) { - identity.IDService = idService + assert.True(t, testRepo().Exists(centIDBytes, newCD.DocumentIdentifier)) + assert.True(t, testRepo().Exists(centIDBytes, newCD.CurrentVersion)) } func createAnchoredMockDocument(t *testing.T, skipSave bool) (*PurchaseOrder, error) { @@ -319,11 +336,20 @@ func createAnchoredMockDocument(t *testing.T, skipSave bool) (*PurchaseOrder, er return nil, err } - sig := signatures.Sign(&config.IdentityConfig{ - ID: centID, + centID, err := identity.ToCentID(centIDBytes) + assert.Nil(t, err) + signKey := identity.IDKey{ PublicKey: key1Pub[:], PrivateKey: key1, - }, corDoc.SigningRoot) + } + idConfig := &identity.IDConfig{ + ID: centID, + Keys: map[int]identity.IDKey{ + identity.KeyPurposeSigning: signKey, + }, + } + + sig := identity.Sign(idConfig, identity.KeyPurposeSigning, corDoc.SigningRoot) corDoc.Signatures = append(corDoc.Signatures, sig) @@ -337,7 +363,7 @@ func createAnchoredMockDocument(t *testing.T, skipSave bool) (*PurchaseOrder, er } if !skipSave { - err = getRepository().Create(i.CoreDocument.CurrentVersion, i) + err = testRepo().Create(centIDBytes, i.CoreDocument.CurrentVersion, i) if err != nil { return nil, err } @@ -347,71 +373,64 @@ func createAnchoredMockDocument(t *testing.T, skipSave bool) (*PurchaseOrder, er } // Functions returns service mocks -func mockSignatureCheck(i *PurchaseOrder, poSrv Service) identity.Service { +func mockSignatureCheck(i *PurchaseOrder, srv *testingcommons.MockIDService, poSrv Service) *testingcommons.MockIDService { idkey := &identity.EthereumIdentityKey{ Key: key1Pub, Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, RevokedAt: big.NewInt(0), } - anchorID, _ := anchors.NewAnchorID(i.CoreDocument.DocumentIdentifier) - docRoot, _ := anchors.NewDocRoot(i.CoreDocument.DocumentRoot) + anchorID, _ := anchors.ToAnchorID(i.CoreDocument.DocumentIdentifier) + docRoot, _ := anchors.ToDocumentRoot(i.CoreDocument.DocumentRoot) mockRepo := poSrv.(service).anchorRepository.(*mockAnchorRepo) mockRepo.On("GetDocumentRootOf", anchorID).Return(docRoot, nil).Once() - srv := &testingcommons.MockIDService{} id := &testingcommons.MockID{} - centID, _ := identity.ToCentID(centID) + centID, _ := identity.ToCentID(centIDBytes) srv.On("LookupIdentityForID", centID).Return(id, nil).Once() id.On("FetchKey", key1Pub[:]).Return(idkey, nil).Once() return srv } func TestService_CreateProofs(t *testing.T) { - poSrv := getServiceWithMockedLayers() - defer setIdentityService(identity.IDService) + idService, poSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, poSrv) - setIdentityService(idService) - proof, err := poSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"po_number"}) + idService = mockSignatureCheck(i, idService, poSrv) + proof, err := poSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"po.po_number"}) assert.Nil(t, err) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentId) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.VersionId) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentID) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.VersionID) assert.Equal(t, len(proof.FieldProofs), 1) - assert.Equal(t, proof.FieldProofs[0].GetProperty(), "po_number") + assert.Equal(t, proof.FieldProofs[0].GetReadableName(), "po.po_number") } func TestService_CreateProofsValidationFails(t *testing.T) { - defer setIdentityService(identity.IDService) - poSrv := getServiceWithMockedLayers() + idService, poSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) i.CoreDocument.SigningRoot = nil - err = getRepository().Update(i.CoreDocument.CurrentVersion, i) + err = testRepo().Update(centIDBytes, i.CoreDocument.CurrentVersion, i) assert.Nil(t, err) - idService := mockSignatureCheck(i, poSrv) - setIdentityService(idService) - _, err = poSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"po_number"}) + idService = mockSignatureCheck(i, idService, poSrv) + _, err = poSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"po.po_number"}) assert.NotNil(t, err) assert.Contains(t, err.Error(), "signing root missing") } func TestService_CreateProofsInvalidField(t *testing.T) { - defer setIdentityService(identity.IDService) - poSrv := getServiceWithMockedLayers() + idService, poSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, poSrv) - setIdentityService(idService) + idService = mockSignatureCheck(i, idService, poSrv) _, err = poSrv.CreateProofs(i.CoreDocument.DocumentIdentifier, []string{"invalid_field"}) assert.Error(t, err) - assert.Equal(t, "createProofs error No such field: invalid_field in obj", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentProof, err)) } func TestService_CreateProofsDocumentDoesntExist(t *testing.T) { - poSrv := getServiceWithMockedLayers() - _, err := poSrv.CreateProofs(utils.RandomSlice(32), []string{"po_number"}) + _, poSrv := getServiceWithMockedLayers() + _, err := poSrv.CreateProofs(utils.RandomSlice(32), []string{"po.po_number"}) assert.Error(t, err) - assert.Equal(t, "document not found: leveldb: not found", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentNotFound, err)) } func updatedAnchoredMockDocument(t *testing.T, model *PurchaseOrder) (*PurchaseOrder, error) { @@ -443,7 +462,7 @@ func updatedAnchoredMockDocument(t *testing.T, model *PurchaseOrder) (*PurchaseO if err != nil { return nil, err } - err = getRepository().Create(model.CoreDocument.CurrentVersion, model) + err = testRepo().Create(centIDBytes, model.CoreDocument.CurrentVersion, model) if err != nil { return nil, err } @@ -451,36 +470,34 @@ func updatedAnchoredMockDocument(t *testing.T, model *PurchaseOrder) (*PurchaseO } func TestService_CreateProofsForVersion(t *testing.T) { - defer setIdentityService(identity.IDService) - poSrv := getServiceWithMockedLayers() + idService, poSrv := getServiceWithMockedLayers() i, err := createAnchoredMockDocument(t, false) assert.Nil(t, err) - idService := mockSignatureCheck(i, poSrv) - setIdentityService(idService) + idService = mockSignatureCheck(i, idService, poSrv) olderVersion := i.CoreDocument.CurrentVersion i, err = updatedAnchoredMockDocument(t, i) assert.Nil(t, err) - proof, err := poSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, olderVersion, []string{"po_number"}) + proof, err := poSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, olderVersion, []string{"po.po_number"}) assert.Nil(t, err) - assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentId) - assert.Equal(t, olderVersion, proof.VersionId) + assert.Equal(t, i.CoreDocument.DocumentIdentifier, proof.DocumentID) + assert.Equal(t, olderVersion, proof.VersionID) assert.Equal(t, len(proof.FieldProofs), 1) - assert.Equal(t, proof.FieldProofs[0].GetProperty(), "po_number") + assert.Equal(t, proof.FieldProofs[0].GetReadableName(), "po.po_number") } func TestService_CreateProofsForVersionDocumentDoesntExist(t *testing.T) { i, err := createAnchoredMockDocument(t, false) - poSrv := getServiceWithMockedLayers() + _, poSrv := getServiceWithMockedLayers() assert.Nil(t, err) - _, err = poSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, utils.RandomSlice(32), []string{"po_number"}) + _, err = poSrv.CreateProofsForVersion(i.CoreDocument.DocumentIdentifier, utils.RandomSlice(32), []string{"po.po_number"}) assert.Error(t, err) - assert.Equal(t, "document not found for the given version: leveldb: not found", err.Error()) + assert.True(t, errors.IsOfType(documents.ErrDocumentVersionNotFound, err)) } func TestService_DerivePurchaseOrderData(t *testing.T) { var m documents.Model - poSrv := getServiceWithMockedLayers() - ctxh, err := documents.NewContextHeader() + _, poSrv := getServiceWithMockedLayers() + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // unknown type @@ -488,7 +505,7 @@ func TestService_DerivePurchaseOrderData(t *testing.T) { d, err := poSrv.DerivePurchaseOrderData(m) assert.Nil(t, d) assert.Error(t, err) - assert.Contains(t, err.Error(), "document of invalid type") + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalidType, err)) // success payload := testingdocuments.CreatePOPayload() @@ -501,12 +518,12 @@ func TestService_DerivePurchaseOrderData(t *testing.T) { func TestService_DerivePurchaseOrderResponse(t *testing.T) { poSrv := service{} - ctxh, err := documents.NewContextHeader() + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // pack fails m := &testingdocuments.MockModel{} - m.On("PackCoreDocument").Return(nil, fmt.Errorf("pack core document failed")).Once() + m.On("PackCoreDocument").Return(nil, errors.New("pack core document failed")).Once() r, err := poSrv.DerivePurchaseOrderResponse(m) m.AssertExpectations(t) assert.Nil(t, r) @@ -532,7 +549,7 @@ func TestService_DerivePurchaseOrderResponse(t *testing.T) { m.AssertExpectations(t) assert.Nil(t, r) assert.Error(t, err) - assert.Contains(t, err.Error(), "document of invalid type") + assert.True(t, errors.IsOfType(documents.ErrDocumentInvalidType, err)) // success payload := testingdocuments.CreatePOPayload() @@ -557,12 +574,14 @@ func createMockDocument() (*PurchaseOrder, error) { NextVersion: nextIdentifier, }, } - err := getRepository().Create(documentIdentifier, model) + err := testRepo().Create(centIDBytes, documentIdentifier, model) return model, err } func TestService_GetCurrentVersion(t *testing.T) { - poSrv := service{repo: getRepository()} + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} thirdIdentifier := utils.RandomSlice(32) doc, err := createMockDocument() assert.Nil(t, err) @@ -582,7 +601,7 @@ func TestService_GetCurrentVersion(t *testing.T) { }, } - err = getRepository().Create(doc.CoreDocument.NextVersion, po2) + err = testRepo().Create(centIDBytes, doc.CoreDocument.NextVersion, po2) assert.Nil(t, err) mod2, err := poSrv.GetCurrentVersion(doc.CoreDocument.DocumentIdentifier) @@ -594,7 +613,9 @@ func TestService_GetCurrentVersion(t *testing.T) { } func TestService_GetVersion_invalid_version(t *testing.T) { - poSrv := service{repo: getRepository()} + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} currentVersion := utils.RandomSlice(32) po := &PurchaseOrder{ @@ -604,16 +625,18 @@ func TestService_GetVersion_invalid_version(t *testing.T) { CurrentVersion: currentVersion, }, } - err := getRepository().Create(currentVersion, po) + err := testRepo().Create(centIDBytes, currentVersion, po) assert.Nil(t, err) mod, err := poSrv.GetVersion(utils.RandomSlice(32), currentVersion) - assert.EqualError(t, err, "[4]document not found for the given version: version is not valid for this identifier") + assert.True(t, errors.IsOfType(documents.ErrDocumentVersionNotFound, err)) assert.Nil(t, mod) } func TestService_GetVersion(t *testing.T) { - poSrv := service{repo: getRepository()} + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} documentIdentifier := utils.RandomSlice(32) currentVersion := utils.RandomSlice(32) @@ -624,7 +647,7 @@ func TestService_GetVersion(t *testing.T) { CurrentVersion: currentVersion, }, } - err := getRepository().Create(currentVersion, po) + err := testRepo().Create(centIDBytes, currentVersion, po) assert.Nil(t, err) mod, err := poSrv.GetVersion(documentIdentifier, currentVersion) @@ -637,6 +660,27 @@ func TestService_GetVersion(t *testing.T) { assert.Error(t, err) } +func TestService_Exists(t *testing.T) { + _, poSrv := getServiceWithMockedLayers() + documentIdentifier := utils.RandomSlice(32) + po := &PurchaseOrder{ + OrderAmount: 42, + CoreDocument: &coredocumentpb.CoreDocument{ + DocumentIdentifier: documentIdentifier, + CurrentVersion: documentIdentifier, + }, + } + err := testRepo().Create(centIDBytes, documentIdentifier, po) + assert.Nil(t, err) + + exists := poSrv.Exists(documentIdentifier) + assert.True(t, exists, "purchase order should exist") + + exists = poSrv.Exists(utils.RandomSlice(32)) + assert.False(t, exists, "purchase order should not exist") + +} + func TestService_ReceiveAnchoredDocument(t *testing.T) { poSrv := service{} err := poSrv.ReceiveAnchoredDocument(nil, nil) @@ -644,15 +688,19 @@ func TestService_ReceiveAnchoredDocument(t *testing.T) { } func TestService_RequestDocumentSignature(t *testing.T) { + ctxh, err := header.NewContextHeader(context.Background(), cfg) + assert.Nil(t, err) poSrv := service{} - s, err := poSrv.RequestDocumentSignature(nil) + s, err := poSrv.RequestDocumentSignature(ctxh, nil) assert.Nil(t, s) assert.Error(t, err) } func TestService_calculateDataRoot(t *testing.T) { - poSrv := service{repo: getRepository()} - ctxh, err := documents.NewContextHeader() + c := &testingconfig.MockConfig{} + c.On("GetIdentityID").Return(centIDBytes, nil) + poSrv := service{config: c, repo: testRepo()} + ctxh, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // type mismatch @@ -666,7 +714,7 @@ func TestService_calculateDataRoot(t *testing.T) { assert.Nil(t, err) assert.Nil(t, po.(*PurchaseOrder).CoreDocument.DataRoot) v := documents.ValidatorFunc(func(_, _ documents.Model) error { - return fmt.Errorf("validations fail") + return errors.New("validations fail") }) po, err = poSrv.calculateDataRoot(nil, po, v) assert.Nil(t, po) @@ -677,12 +725,12 @@ func TestService_calculateDataRoot(t *testing.T) { po, err = poSrv.DeriveFromCreatePayload(testingdocuments.CreatePOPayload(), ctxh) assert.Nil(t, err) assert.Nil(t, po.(*PurchaseOrder).CoreDocument.DataRoot) - err = poSrv.repo.Create(po.(*PurchaseOrder).CoreDocument.CurrentVersion, po) + err = poSrv.repo.Create(centIDBytes, po.(*PurchaseOrder).CoreDocument.CurrentVersion, po) assert.Nil(t, err) po, err = poSrv.calculateDataRoot(nil, po, CreateValidator()) assert.Nil(t, po) assert.Error(t, err) - assert.Contains(t, err.Error(), "document already exists") + assert.Contains(t, err.Error(), documents.ErrDocumentRepositoryModelAllReadyExists) // success po, err = poSrv.DeriveFromCreatePayload(testingdocuments.CreatePOPayload(), ctxh) @@ -693,3 +741,17 @@ func TestService_calculateDataRoot(t *testing.T) { assert.NotNil(t, po) assert.NotNil(t, po.(*PurchaseOrder).CoreDocument.DataRoot) } + +var testRepoGlobal documents.Repository + +func testRepo() documents.Repository { + if testRepoGlobal == nil { + ldb, err := storage.NewLevelDBStorage(storage.GetRandomTestStoragePath()) + if err != nil { + panic(err) + } + testRepoGlobal = documents.NewLevelDBRepository(ldb) + testRepoGlobal.Register(&PurchaseOrder{}) + } + return testRepoGlobal +} diff --git a/documents/purchaseorder/validator.go b/documents/purchaseorder/validator.go index 0881a07e5..bba33373b 100644 --- a/documents/purchaseorder/validator.go +++ b/documents/purchaseorder/validator.go @@ -1,10 +1,9 @@ package purchaseorder import ( - "fmt" - "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" ) @@ -12,17 +11,17 @@ import ( func fieldValidator() documents.Validator { return documents.ValidatorFunc(func(_, new documents.Model) error { if new == nil { - return fmt.Errorf("nil document") + return errors.New("nil document") } - inv, ok := new.(*PurchaseOrder) + po, ok := new.(*PurchaseOrder) if !ok { - return fmt.Errorf("unknown document type") + return errors.New("unknown document type") } var err error - if !documents.IsCurrencyValid(inv.Currency) { - err = documents.AppendError(err, documents.NewError("po_currency", "currency is invalid")) + if !documents.IsCurrencyValid(po.Currency) { + err = errors.AppendError(err, documents.NewError("po_currency", "currency is invalid")) } return err @@ -34,34 +33,34 @@ func dataRootValidator() documents.Validator { return documents.ValidatorFunc(func(_, model documents.Model) (err error) { defer func() { if err != nil { - err = fmt.Errorf("data root validation failed: %v", err) + err = errors.New("data root validation failed: %v", err) } }() if model == nil { - return fmt.Errorf("nil document") + return errors.New("nil document") } coreDoc, err := model.PackCoreDocument() if err != nil { - return fmt.Errorf("failed to pack coredocument: %v", err) + return errors.New("failed to pack coredocument: %v", err) } if utils.IsEmptyByteSlice(coreDoc.DataRoot) { - return fmt.Errorf("data root missing") + return errors.New("data root missing") } inv, ok := model.(*PurchaseOrder) if !ok { - return fmt.Errorf("unknown document type: %T", model) + return errors.New("unknown document type: %T", model) } if err = inv.calculateDataRoot(); err != nil { - return fmt.Errorf("failed to calculate data root: %v", err) + return errors.New("failed to calculate data root: %v", err) } if !utils.IsSameByteSlice(inv.CoreDocument.DataRoot, coreDoc.DataRoot) { - return fmt.Errorf("mismatched data root") + return errors.New("mismatched data root") } return nil diff --git a/documents/purchaseorder/validator_test.go b/documents/purchaseorder/validator_test.go index 343ae641b..0dc56c63c 100644 --- a/documents/purchaseorder/validator_test.go +++ b/documents/purchaseorder/validator_test.go @@ -3,11 +3,13 @@ package purchaseorder import ( - "fmt" "testing" + "context" + "github.com/centrifuge/go-centrifuge/coredocument" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" @@ -19,21 +21,21 @@ func TestFieldValidator_Validate(t *testing.T) { // nil error err := fv.Validate(nil, nil) assert.Error(t, err) - errs := documents.Errors(err) + errs := errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be one") assert.Contains(t, errs[0].Error(), "nil document") // unknown type err = fv.Validate(nil, &testingdocuments.MockModel{}) assert.Error(t, err) - errs = documents.Errors(err) + errs = errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be one") assert.Contains(t, errs[0].Error(), "unknown document type") // fail err = fv.Validate(nil, new(PurchaseOrder)) assert.Error(t, err) - errs = documents.Errors(err) + errs = errors.GetErrs(err) assert.Len(t, errs, 1, "errors length must be 2") assert.Contains(t, errs[0].Error(), "currency is invalid") @@ -46,7 +48,7 @@ func TestFieldValidator_Validate(t *testing.T) { func TestDataRootValidation_Validate(t *testing.T) { drv := dataRootValidator() - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) // nil error @@ -56,7 +58,7 @@ func TestDataRootValidation_Validate(t *testing.T) { // pack coredoc failed model := &testingdocuments.MockModel{} - model.On("PackCoreDocument").Return(nil, fmt.Errorf("error")).Once() + model.On("PackCoreDocument").Return(nil, errors.New("error")).Once() err = drv.Validate(nil, model) model.AssertExpectations(t) assert.Error(t, err) diff --git a/documents/registry.go b/documents/registry.go index c11fa9419..49848237d 100644 --- a/documents/registry.go +++ b/documents/registry.go @@ -1,8 +1,9 @@ package documents import ( - "fmt" "sync" + + "github.com/centrifuge/go-centrifuge/errors" ) //ServiceRegistry matches for a provided coreDocument the corresponding service @@ -11,17 +12,11 @@ type ServiceRegistry struct { mutex sync.RWMutex } -var registryInstance *ServiceRegistry -var registryOnce sync.Once - -// GetRegistryInstance returns current registry instance -func GetRegistryInstance() *ServiceRegistry { - registryOnce.Do(func() { - registryInstance = &ServiceRegistry{} - registryInstance.services = make(map[string]Service) - registryInstance.mutex = sync.RWMutex{} - }) - return registryInstance +// NewServiceRegistry returns a new instance of service registry +func NewServiceRegistry() *ServiceRegistry { + return &ServiceRegistry{ + services: make(map[string]Service), + } } // Register can register a service which implements the ModelDeriver interface @@ -29,7 +24,7 @@ func (s *ServiceRegistry) Register(serviceID string, service Service) error { s.mutex.Lock() defer s.mutex.Unlock() if _, ok := s.services[serviceID]; ok { - return fmt.Errorf("service with provided id already registered") + return errors.New("service with provided id already registered") } s.services[serviceID] = service @@ -41,8 +36,26 @@ func (s *ServiceRegistry) LocateService(serviceID string) (Service, error) { s.mutex.RLock() defer s.mutex.RUnlock() if s.services[serviceID] == nil { - return nil, fmt.Errorf("no service for core document type is registered") + return nil, errors.New("no service for core document type is registered") } return s.services[serviceID], nil } + +// FindService will search the service based on the documentID +func (s *ServiceRegistry) FindService(documentID []byte) (Service, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + for _, service := range s.services { + + exists := service.Exists(documentID) + + if exists { + return service, nil + } + + } + return nil, errors.New("no service exists for provided documentID") + +} diff --git a/documents/registry_test.go b/documents/registry_test.go index 474b14ee6..5501b5154 100644 --- a/documents/registry_test.go +++ b/documents/registry_test.go @@ -7,47 +7,44 @@ import ( cd "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/stretchr/testify/assert" ) -func TestRegistry_GetRegistryInstance(t *testing.T) { - - registryFirst := documents.GetRegistryInstance() - registrySecond := documents.GetRegistryInstance() - - assert.Equal(t, ®istryFirst, ®istrySecond, "only one instance of registry should exist") +func TestServiceRegistry_FindService(t *testing.T) { + registry := documents.NewServiceRegistry() + a := &testingdocuments.MockService{} + b := &testingdocuments.MockService{} + a.On("Exists").Return(true) + b.On("Exists").Return(false) + err := registry.Register("a service", a) + err = registry.Register("b service", b) + + service, err := registry.FindService([]byte{}) + assert.Nil(t, err, "findService should be successful") + assert.Equal(t, a, service, "service a should be returned") } func TestRegistry_Register_LocateService_successful(t *testing.T) { - - registry := documents.GetRegistryInstance() - + registry := documents.NewServiceRegistry() a := &testingdocuments.MockService{} - - coreDocument := testingutils.GenerateCoreDocument() + coreDocument := testingcoredocument.GenerateCoreDocument() documentType, err := cd.GetTypeURL(coreDocument) assert.Nil(t, err, "should not throw an error because core document contains a type") err = registry.Register(documentType, a) - assert.Nil(t, err, "register didn't work with unused id") b, err := registry.LocateService(documentType) assert.Nil(t, err, "service hasn't been registered properly") - assert.Equal(t, a, b, "locateService should return the same service ") - } func TestRegistry_Register_invalidId(t *testing.T) { - - registry := documents.GetRegistryInstance() - + registry := documents.NewServiceRegistry() a := &testingdocuments.MockService{} - - coreDocument := testingutils.GenerateCoreDocument() + coreDocument := testingcoredocument.GenerateCoreDocument() coreDocument.EmbeddedData.TypeUrl = "testID_1" err := registry.Register(coreDocument.EmbeddedData.TypeUrl, a) @@ -58,18 +55,15 @@ func TestRegistry_Register_invalidId(t *testing.T) { err = registry.Register("testId", a) assert.Nil(t, err, "register didn't work with unused id") - } func TestRegistry_LocateService_invalid(t *testing.T) { - - registry := documents.GetRegistryInstance() - coreDocument := testingutils.GenerateCoreDocument() + registry := documents.NewServiceRegistry() + coreDocument := testingcoredocument.GenerateCoreDocument() coreDocument.EmbeddedData.TypeUrl = "testID_2" documentType, err := cd.GetTypeURL(coreDocument) assert.Nil(t, err, "should not throw an error because core document contains a type") _, err = registry.LocateService(documentType) assert.Error(t, err, "should throw an error because no services is registered") - } diff --git a/documents/repository.go b/documents/repository.go index a27372b57..5e7a3d254 100644 --- a/documents/repository.go +++ b/documents/repository.go @@ -1,39 +1,173 @@ package documents -// Loader interface can be implemented by any type that handles document retrieval -type Loader interface { - // GetKey will prepare the the identifier key from ID - GetKey(id []byte) (key []byte) +import ( + "encoding/json" + "reflect" + "sync" - // GetByID finds the doc with identifier and marshals it into message - LoadByID(id []byte, model Model) error + "github.com/centrifuge/go-centrifuge/errors" + "github.com/syndtr/goleveldb/leveldb" +) + +// Repository defines the required methods for a document repository. +// Can be implemented by any type that stores the documents. Ex: levelDB, sql etc... +type Repository interface { + // Exists checks if the id, owned by tenantID, exists in DB + Exists(tenantID, id []byte) bool + + // Get returns the Model associated with ID, owned by tenantID + Get(tenantID, id []byte) (Model, error) + + // Create creates the model if not present in the DB. + // should error out if the document exists. + Create(tenantID, id []byte, model Model) error + + // Update strictly updates the model. + // Will error out when the model doesn't exist in the DB. + Update(tenantID, id []byte, model Model) error + + // Register registers the model so that the DB can return the document without knowing the type + Register(model Model) } -// Checker interface can be implemented by any type that handles if document exists -type Checker interface { - // Exists checks for document existence - // True if exists else false - Exists(id []byte) bool +// levelDBRepo implements Repository using LevelDB as storage layer +type levelDBRepo struct { + db *leveldb.DB + models map[string]reflect.Type + mu sync.RWMutex // to protect the models } -// Creator interface can be implemented by any type that handles document creation -type Creator interface { - // Create stores the initial document - // If document exist, it errors out - Create(id []byte, model Model) error +// value is an internal representation of how levelDb stores the model. +type value struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` } -// Updater interface can be implemented by any type that handles document update -type Updater interface { - // Update updates the already stored document - // errors out when document is missing - Update(id []byte, model Model) error +// NewLevelDBRepository returns levelDb implementation of Repository +func NewLevelDBRepository(db *leveldb.DB) Repository { + return &levelDBRepo{ + db: db, + models: make(map[string]reflect.Type), + } } -// Repository should be implemented by any type that wants to store a document in key-value storage -type Repository interface { - Checker - Loader - Creator - Updater +// getKey returns tenantID+id +func getKey(tenantID, id []byte) []byte { + return append(tenantID, id...) +} + +// Exists returns true if the id, owned by tenantID, exists. +func (l *levelDBRepo) Exists(tenantID, id []byte) bool { + key := getKey(tenantID, id) + res, err := l.db.Has(key, nil) + // TODO check this + if err != nil { + return false + } + + return res +} + +// getModel returns a new instance of the type mt. +func (l *levelDBRepo) getModel(mt string) (Model, error) { + tp, ok := l.models[mt] + if !ok { + return nil, errors.NewTypedError(ErrDocumentRepositoryModelNotRegistered, errors.New("type %s not registered", mt)) + } + + return reflect.New(tp).Interface().(Model), nil +} + +// Get returns the model associated with ID, owned by tenantID. +func (l *levelDBRepo) Get(tenantID, id []byte) (Model, error) { + key := getKey(tenantID, id) + data, err := l.db.Get(key, nil) + if err != nil { + return nil, errors.NewTypedError(ErrDocumentRepositoryModelNotFound, errors.New("document missing: %v", err)) + } + + v := new(value) + err = json.Unmarshal(data, v) + if err != nil { + return nil, errors.NewTypedError(ErrDocumentRepositorySerialisation, errors.New("failed to unmarshal value: %v", err)) + } + + l.mu.RLock() + defer l.mu.RUnlock() + nm, err := l.getModel(v.Type) + if err != nil { + return nil, err + } + + err = nm.FromJSON([]byte(v.Data)) + if err != nil { + return nil, errors.NewTypedError(ErrDocumentRepositorySerialisation, errors.New("failed to unmarshal to model: %v", err)) + } + + return nm, nil +} + +// getTypeIndirect returns the type of the model without pointers. +func getTypeIndirect(tp reflect.Type) reflect.Type { + if tp.Kind() == reflect.Ptr { + return getTypeIndirect(tp.Elem()) + } + + return tp +} + +// save stores the model. +func (l *levelDBRepo) save(tenantID, id []byte, model Model) error { + data, err := model.JSON() + if err != nil { + return errors.NewTypedError(ErrDocumentRepositorySerialisation, errors.New("failed to marshall model: %v", err)) + } + + tp := getTypeIndirect(model.Type()) + v := value{ + Type: tp.String(), + Data: json.RawMessage(data), + } + + data, err = json.Marshal(v) + if err != nil { + return errors.NewTypedError(ErrDocumentRepositorySerialisation, errors.New("failed to marshall value: %v", err)) + } + + key := getKey(tenantID, id) + err = l.db.Put(key, data, nil) + if err != nil { + return errors.NewTypedError(ErrDocumentRepositoryModelSave, errors.New("%v", err)) + } + + return nil +} + +// Create stores the model to the DB. +// Errors out if the model already exists. +func (l *levelDBRepo) Create(tenantID, id []byte, model Model) error { + if l.Exists(tenantID, id) { + return ErrDocumentRepositoryModelAllReadyExists + } + + return l.save(tenantID, id, model) +} + +// Update overwrites the value at tenantID+id. +// Errors out if model doesn't exist +func (l *levelDBRepo) Update(tenantID, id []byte, model Model) error { + if !l.Exists(tenantID, id) { + return ErrDocumentRepositoryModelDoesntExist + } + + return l.save(tenantID, id, model) +} + +// Register registers the model for type less operations. +// Same type names will be overwritten. +func (l *levelDBRepo) Register(model Model) { + l.mu.Lock() + defer l.mu.Unlock() + tp := getTypeIndirect(model.Type()) + l.models[tp.String()] = tp } diff --git a/documents/repository_test.go b/documents/repository_test.go new file mode 100644 index 000000000..b6da2d616 --- /dev/null +++ b/documents/repository_test.go @@ -0,0 +1,110 @@ +// +build unit + +package documents + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb" +) + +func getRepository(ctx map[string]interface{}) Repository { + db := ctx[storage.BootstrappedLevelDB].(*leveldb.DB) + return NewLevelDBRepository(db) +} + +type doc struct { + Model + SomeString string `json:"some_string"` +} + +func (m *doc) JSON() ([]byte, error) { + return json.Marshal(m) +} + +func (m *doc) FromJSON(data []byte) error { + return json.Unmarshal(data, m) +} + +func (m *doc) Type() reflect.Type { + return reflect.TypeOf(m) +} + +func TestLevelDBRepo_Create_Exists(t *testing.T) { + repo := getRepository(ctx) + d := &doc{SomeString: "Hello, World!"} + tenantID, id := utils.RandomSlice(32), utils.RandomSlice(32) + assert.False(t, repo.Exists(tenantID, id), "doc must not be present") + err := repo.Create(tenantID, id, d) + assert.Nil(t, err, "Create: unknown error") + assert.True(t, repo.Exists(tenantID, id), "doc must be present") + + // overwrite + err = repo.Create(tenantID, id, d) + assert.Error(t, err, "Create: must not overwrite existing doc") +} + +func TestLevelDBRepo_Update_Exists(t *testing.T) { + repo := getRepository(ctx) + d := &doc{SomeString: "Hello, World!"} + tenantID, id := utils.RandomSlice(32), utils.RandomSlice(32) + assert.False(t, repo.Exists(tenantID, id), "doc must not be present") + err := repo.Update(tenantID, id, d) + assert.Error(t, err, "Update: should error out") + assert.False(t, repo.Exists(tenantID, id), "doc must not be present") + + // overwrite + err = repo.Create(tenantID, id, d) + assert.Nil(t, err, "Create: unknown error") + d.SomeString = "Hello, Repo!" + err = repo.Update(tenantID, id, d) + assert.Nil(t, err, "Update: unknown error") + assert.True(t, repo.Exists(tenantID, id), "doc must be [resent") +} + +func TestLevelDBRepo_Register(t *testing.T) { + repo := getRepository(ctx) + assert.Len(t, repo.(*levelDBRepo).models, 0, "should be empty") + d := &doc{SomeString: "Hello, Repo!"} + repo.Register(d) + assert.Len(t, repo.(*levelDBRepo).models, 1, "should be not empty") + assert.Contains(t, repo.(*levelDBRepo).models, "documents.doc") +} + +func TestLevelDBRepo_Get_Create_Update(t *testing.T) { + repo := getRepository(ctx) + + tenantID, id := utils.RandomSlice(32), utils.RandomSlice(32) + m, err := repo.Get(tenantID, id) + assert.Error(t, err, "must return error") + assert.Nil(t, m) + + d := &doc{SomeString: "Hello, Repo!"} + err = repo.Create(tenantID, id, d) + assert.Nil(t, err, "Create: unknown error") + + m, err = repo.Get(tenantID, id) + assert.Error(t, err, "doc is not registered yet") + assert.Nil(t, m) + + repo.Register(&doc{}) + m, err = repo.Get(tenantID, id) + assert.Nil(t, err) + assert.NotNil(t, m) + nd := m.(*doc) + assert.Equal(t, d, nd, "must be equal") + + d.SomeString = "Hello, World!" + err = repo.Update(tenantID, id, d) + assert.Nil(t, err, "Update: unknown error") + + m, err = repo.Get(tenantID, id) + assert.Nil(t, err, "Get: unknown error") + nd = m.(*doc) + assert.Equal(t, d, nd, "must be equal") +} diff --git a/documents/service.go b/documents/service.go index cac0c7830..cbe126990 100644 --- a/documents/service.go +++ b/documents/service.go @@ -3,13 +3,21 @@ package documents import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/precise-proofs/proofs/proto" ) +// Config specified configs required by documents package +type Config interface { + + // GetIdentityID retrieves the centID(TenentID) configured + GetIdentityID() ([]byte, error) +} + // DocumentProof is a value to represent a document and its field proofs type DocumentProof struct { - DocumentId []byte - VersionId []byte + DocumentID []byte + VersionID []byte State string FieldProofs []*proofspb.Proof } @@ -20,6 +28,9 @@ type Service interface { // GetCurrentVersion reads a document from the database GetCurrentVersion(documentID []byte) (Model, error) + // Exists checks if a document exists + Exists(documentID []byte) bool + // GetVersion reads a document from the database GetVersion(documentID []byte, version []byte) (Model, error) @@ -33,7 +44,7 @@ type Service interface { CreateProofsForVersion(documentID, version []byte, fields []string) (*DocumentProof, error) // RequestDocumentSignature Validates and Signs document received over the p2p layer - RequestDocumentSignature(model Model) (*coredocumentpb.Signature, error) + RequestDocumentSignature(contextHeader *header.ContextHeader, model Model) (*coredocumentpb.Signature, error) // ReceiveAnchoredDocument receives a new anchored document over the p2p layer, validates and updates the document in DB ReceiveAnchoredDocument(model Model, headers *p2ppb.CentrifugeHeader) error diff --git a/documents/anchor_test.go b/documents/test/anchor_test.go similarity index 56% rename from documents/anchor_test.go rename to documents/test/anchor_test.go index 35d5c071f..ab33cfa0c 100644 --- a/documents/anchor_test.go +++ b/documents/test/anchor_test.go @@ -4,26 +4,47 @@ package documents_test import ( "context" - "fmt" + "errors" "testing" + "os" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/header" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/testingutils/documents" "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + +func TestMain(m *testing.M) { + ibootstappers := []bootstrap.TestBootstrapper{ + &config.Bootstrapper{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} + func TestAnchorDocument(t *testing.T) { ctx := context.Background() + ctxh, err := header.NewContextHeader(ctx, cfg) + assert.Nil(t, err) updater := func(id []byte, model documents.Model) error { return nil } // pack fails m := &testingdocuments.MockModel{} - m.On("PackCoreDocument").Return(nil, fmt.Errorf("pack failed")).Once() - model, err := documents.AnchorDocument(ctx, m, nil, updater) + m.On("PackCoreDocument").Return(nil, errors.New("pack failed")).Once() + model, err := documents.AnchorDocument(ctxh, m, nil, updater) m.AssertExpectations(t) assert.Nil(t, model) assert.Error(t, err) @@ -33,9 +54,9 @@ func TestAnchorDocument(t *testing.T) { m = &testingdocuments.MockModel{} cd := coredocument.New() m.On("PackCoreDocument").Return(cd, nil).Once() - proc := &testingutils.MockCoreDocumentProcessor{} - proc.On("PrepareForSignatureRequests", m).Return(fmt.Errorf("error")).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc := &testingcoredocument.MockCoreDocumentProcessor{} + proc.On("PrepareForSignatureRequests", m).Return(errors.New("error")).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, model) @@ -45,10 +66,10 @@ func TestAnchorDocument(t *testing.T) { // request signatures failed m = &testingdocuments.MockModel{} m.On("PackCoreDocument").Return(cd, nil).Once() - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", m).Return(nil).Once() - proc.On("RequestSignatures", ctx, m).Return(fmt.Errorf("error")).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc.On("RequestSignatures", ctxh, m).Return(errors.New("error")).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, model) @@ -58,11 +79,11 @@ func TestAnchorDocument(t *testing.T) { // prepare for anchoring fails m = &testingdocuments.MockModel{} m.On("PackCoreDocument").Return(cd, nil).Once() - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", m).Return(nil).Once() - proc.On("RequestSignatures", ctx, m).Return(nil).Once() - proc.On("PrepareForAnchoring", m).Return(fmt.Errorf("error")).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc.On("RequestSignatures", ctxh, m).Return(nil).Once() + proc.On("PrepareForAnchoring", m).Return(errors.New("error")).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, model) @@ -72,12 +93,12 @@ func TestAnchorDocument(t *testing.T) { // anchor fails m = &testingdocuments.MockModel{} m.On("PackCoreDocument").Return(cd, nil).Once() - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", m).Return(nil).Once() - proc.On("RequestSignatures", ctx, m).Return(nil).Once() + proc.On("RequestSignatures", ctxh, m).Return(nil).Once() proc.On("PrepareForAnchoring", m).Return(nil).Once() - proc.On("AnchorDocument", m).Return(fmt.Errorf("error")).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc.On("AnchorDocument", m).Return(errors.New("error")).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, model) @@ -87,13 +108,13 @@ func TestAnchorDocument(t *testing.T) { // send failed m = &testingdocuments.MockModel{} m.On("PackCoreDocument").Return(cd, nil).Once() - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", m).Return(nil).Once() - proc.On("RequestSignatures", ctx, m).Return(nil).Once() + proc.On("RequestSignatures", ctxh, m).Return(nil).Once() proc.On("PrepareForAnchoring", m).Return(nil).Once() proc.On("AnchorDocument", m).Return(nil).Once() - proc.On("SendDocument", ctx, m).Return(fmt.Errorf("error")).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc.On("SendDocument", ctxh, m).Return(errors.New("error")).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, model) @@ -103,13 +124,13 @@ func TestAnchorDocument(t *testing.T) { // success m = &testingdocuments.MockModel{} m.On("PackCoreDocument").Return(cd, nil).Once() - proc = &testingutils.MockCoreDocumentProcessor{} + proc = &testingcoredocument.MockCoreDocumentProcessor{} proc.On("PrepareForSignatureRequests", m).Return(nil).Once() - proc.On("RequestSignatures", ctx, m).Return(nil).Once() + proc.On("RequestSignatures", ctxh, m).Return(nil).Once() proc.On("PrepareForAnchoring", m).Return(nil).Once() proc.On("AnchorDocument", m).Return(nil).Once() - proc.On("SendDocument", ctx, m).Return(nil).Once() - model, err = documents.AnchorDocument(ctx, m, proc, updater) + proc.On("SendDocument", ctxh, m).Return(nil).Once() + model, err = documents.AnchorDocument(ctxh, m, proc, updater) m.AssertExpectations(t) proc.AssertExpectations(t) assert.Nil(t, err) diff --git a/documents/test_bootstrapper.go b/documents/test_bootstrapper.go index 29201cd4b..d0c4a71aa 100644 --- a/documents/test_bootstrapper.go +++ b/documents/test_bootstrapper.go @@ -3,23 +3,23 @@ package documents import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" - "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/storage" "github.com/syndtr/goleveldb/leveldb" ) // initialized ONLY for tests var testLevelDB Repository -func (b *Bootstrapper) TestBootstrap(context map[string]interface{}) error { - if _, ok := context[bootstrap.BootstrappedLevelDb]; !ok { +func (b Bootstrapper) TestBootstrap(context map[string]interface{}) error { + if _, ok := context[storage.BootstrappedLevelDB]; !ok { return errors.New("initializing LevelDB repository failed") } - testLevelDB = LevelDBRepository{LevelDB: context[bootstrap.BootstrappedLevelDb].(*leveldb.DB)} + testLevelDB = NewLevelDBRepository(context[storage.BootstrappedLevelDB].(*leveldb.DB)) return b.Bootstrap(context) } -func (*Bootstrapper) TestTearDown() error { +func (Bootstrapper) TestTearDown() error { return nil } diff --git a/documents/validator.go b/documents/validator.go index df04de457..d2256d06e 100644 --- a/documents/validator.go +++ b/documents/validator.go @@ -1,6 +1,7 @@ package documents import ( + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" ) @@ -14,14 +15,14 @@ type Validator interface { type ValidatorGroup []Validator //Validate will execute all group specific atomic validations -func (group ValidatorGroup) Validate(oldState Model, newState Model) (errors error) { +func (group ValidatorGroup) Validate(oldState Model, newState Model) (errs error) { for _, v := range group { if err := v.Validate(oldState, newState); err != nil { - errors = AppendError(errors, err) + errs = errors.AppendError(errs, err) } } - return errors + return errs } // IsCurrencyValid checks if the currency is of length 3 diff --git a/documents/validator_test.go b/documents/validator_test.go index 5759f60f3..34b07e8f0 100644 --- a/documents/validator_test.go +++ b/documents/validator_test.go @@ -3,9 +3,9 @@ package documents import ( - "fmt" "testing" + "github.com/centrifuge/go-centrifuge/errors" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ type MockValidatorWithErrors struct{} func (m MockValidatorWithErrors) Validate(oldState Model, newState Model) error { err := NewError("error_test", "error msg 1") - err = AppendError(err, NewError("error_test2", "error msg 2")) + err = errors.AppendError(err, NewError("error_test2", "error msg 2")) return err } @@ -28,7 +28,7 @@ func (m MockValidatorWithErrors) Validate(oldState Model, newState Model) error type MockValidatorWithOneError struct{} func (m MockValidatorWithOneError) Validate(oldState Model, newState Model) error { - return fmt.Errorf("one error") + return errors.New("one error") } func TestValidatorInterface(t *testing.T) { @@ -36,23 +36,20 @@ func TestValidatorInterface(t *testing.T) { // no error validator = MockValidator{} - errors := validator.Validate(nil, nil) - assert.Nil(t, errors, "") + errs := validator.Validate(nil, nil) + assert.Nil(t, errs, "") //one error validator = MockValidatorWithOneError{} - errors = validator.Validate(nil, nil) - assert.Error(t, errors, "error should be returned") - assert.Equal(t, 1, LenError(errors), "errors should include one error") + errs = validator.Validate(nil, nil) + assert.Error(t, errs, "error should be returned") + assert.Equal(t, 1, errors.Len(errs), "errors should include one error") // more than one error validator = MockValidatorWithErrors{} - errors = validator.Validate(nil, nil) - assert.Error(t, errors, "error should be returned") - assert.Equal(t, 2, LenError(errors), "errors should include two error") - - errorArray := Errors(errors) - assert.Equal(t, 2, len(errorArray), "error array should include two error") + errs = validator.Validate(nil, nil) + assert.Error(t, errs, "error should be returned") + assert.Equal(t, 2, errors.Len(errs), "errors should include two errors") } func TestValidatorGroup_Validate(t *testing.T) { @@ -62,21 +59,21 @@ func TestValidatorGroup_Validate(t *testing.T) { MockValidatorWithOneError{}, MockValidatorWithErrors{}, } - errors := testValidatorGroup.Validate(nil, nil) - assert.Equal(t, 3, len(Errors(errors)), "Validate should return 2 errors") + errs := testValidatorGroup.Validate(nil, nil) + assert.Equal(t, 3, errors.Len(errs), "Validate should return 2 errors") testValidatorGroup = ValidatorGroup{ MockValidator{}, MockValidatorWithErrors{}, MockValidatorWithErrors{}, } - errors = testValidatorGroup.Validate(nil, nil) - assert.Equal(t, 4, len(Errors(errors)), "Validate should return 4 errors") + errs = testValidatorGroup.Validate(nil, nil) + assert.Equal(t, 4, errors.Len(errs), "Validate should return 4 errors") // empty group testValidatorGroup = ValidatorGroup{} - errors = testValidatorGroup.Validate(nil, nil) - assert.Equal(t, 0, len(Errors(errors)), "Validate should return no error") + errs = testValidatorGroup.Validate(nil, nil) + assert.Equal(t, 0, errors.Len(errs), "Validate should return no error") // group with no errors at all testValidatorGroup = ValidatorGroup{ @@ -84,8 +81,8 @@ func TestValidatorGroup_Validate(t *testing.T) { MockValidator{}, MockValidator{}, } - errors = testValidatorGroup.Validate(nil, nil) - assert.Equal(t, 0, len(Errors(errors)), "Validate should return no error") + errs = testValidatorGroup.Validate(nil, nil) + assert.Equal(t, 0, errors.Len(errs), "Validate should return no error") } func TestIsCurrencyValid(t *testing.T) { diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 000000000..f4afa817b --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,141 @@ +package errors + +import ( + "fmt" + "strings" +) + +// ErrUnknown is an unknown error type +const ErrUnknown = Error("unknown error") + +// Error is a string that implements error +// this will have interesting side effects of having constant errors +type Error string + +// Error returns error message +func (e Error) Error() string { + return string(e) +} + +// New returns a new error with message passed. +// if args are passed, we will format the message with args +// Example: +// New("some error") returns error with exact string passed +// New("some error: %v", "some context") returns error with message "some error: some context" +func New(format string, args ...interface{}) error { + return Error(fmt.Sprintf(format, args...)) +} + +// listError holds a list of errors +type listError []error + +// Error formats the underlying errs into string +func (l listError) Error() string { + if len(l) == 0 { + return "" + } + + var errs []string + for _, err := range l { + errs = append(errs, err.Error()) + } + + res := strings.Join(errs, "; ") + return "[" + res + "]" +} + +// GetErrs gets the list of errors if its a list +func GetErrs(err error) []error { + if err == nil { + return nil + } + + if errl, ok := err.(listError); ok { + return errl + } + + return []error{err} +} + +// AppendError returns a new listError +// if errn == nil, return err +// if err is of type listError and if errn is of type listerror, +// append errn errors to err and return err +func AppendError(err, errn error) error { + var errs listError + + for _, e := range []error{err, errn} { + if serrs := GetErrs(e); len(serrs) > 0 { + errs = append(errs, serrs...) + } + } + + if len(errs) < 1 { + return nil + } + + return errs +} + +// Len returns the total number of errors +// if err is listError, return len(listError.errs) +// if err == nil, return 0 +// else return 1 +func Len(err error) int { + if err == nil { + return 0 + } + + if lerr, ok := err.(listError); ok { + return len(lerr) + } + + return 1 +} + +// typedError holds a type of error and an context error +type typedError struct { + terr error + ctxErr error +} + +// Error returns the error in string +func (t *typedError) Error() string { + return fmt.Sprintf("%v: %v", t.terr, t.ctxErr) +} + +// NewTypedError returns a new error of type typedError +func NewTypedError(terr, err error) error { + if terr == nil { + terr = ErrUnknown + } + + return &typedError{terr: terr, ctxErr: err} +} + +// TypedError can be implemented by any type error +type TypedError interface { + IsOfType(terr error) bool +} + +// IsOfType returns if the err t is of type terr +func (t *typedError) IsOfType(terr error) bool { + if t.terr.Error() == terr.Error() { + return true + } + + if cterr, ok := t.ctxErr.(TypedError); ok { + return cterr.IsOfType(terr) + } + + return t.ctxErr.Error() == terr.Error() +} + +// IsOfType returns if the err is of type terr +func IsOfType(terr, err error) bool { + if errt, ok := err.(TypedError); ok { + return errt.IsOfType(terr) + } + + return err.Error() == terr.Error() +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 000000000..811b5c450 --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,112 @@ +// +build unit + +package errors + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + tests := []struct { + msg string + args []interface{} + result string + }{ + // empty message + {}, + + //// simple message + { + msg: "some error", + result: "some error", + }, + + // format error + { + msg: "some error: %v: %v", + args: []interface{}{"error1", "error2"}, + result: "some error: error1: error2", + }, + } + + for _, c := range tests { + err := New(c.msg, c.args...) + assert.Equal(t, c.result, err.Error(), "must match") + } +} + +func checkListError(t *testing.T, lerr error, len int, result string) { + assert.NotNil(t, lerr) + _, ok := lerr.(listError) + assert.True(t, ok) + assert.Equal(t, len, Len(lerr)) + assert.Equal(t, result, lerr.Error()) +} + +func TestAppendError(t *testing.T) { + // both nil + assert.Nil(t, AppendError(nil, nil)) + + // errn nil, and err not nil but simple error + serr := New("some error") + lerr := AppendError(serr, nil) + checkListError(t, lerr, 1, "[some error]") + + // errn nil, and err not nil and a list error + lerr = AppendError(lerr, nil) + checkListError(t, lerr, 1, "[some error]") + + // err nil and errn not nil and simple error + lerr = AppendError(nil, serr) + checkListError(t, lerr, 1, "[some error]") + + // err nil and errn not nil and list error + lerr = AppendError(nil, lerr) + checkListError(t, lerr, 1, "[some error]") + + // both simple errors + lerr = AppendError(serr, serr) + checkListError(t, lerr, 2, "[some error; some error]") + + // err simple and errn list + lerr = AppendError(serr, lerr) + checkListError(t, lerr, 3, "[some error; some error; some error]") + + // err list error and errn simple error + lerr = AppendError(lerr, serr) + checkListError(t, lerr, 4, "[some error; some error; some error; some error]") + + // both list errors + lerr = AppendError(AppendError(serr, nil), AppendError(serr, nil)) + checkListError(t, lerr, 2, "[some error; some error]") +} + +func TestIsOfType(t *testing.T) { + // single type error + const errBadErr = Error("bad error") + serr := New("some error") + terr := NewTypedError(ErrUnknown, serr) + assert.True(t, IsOfType(ErrUnknown, terr)) + assert.False(t, IsOfType(errBadErr, terr)) + assert.Equal(t, "unknown error: some error", terr.Error()) + + // recursive error + terr = NewTypedError(errBadErr, terr) + assert.True(t, IsOfType(ErrUnknown, terr)) + assert.True(t, IsOfType(errBadErr, terr)) + assert.Equal(t, "bad error: unknown error: some error", terr.Error()) + + // simple error + assert.False(t, IsOfType(ErrUnknown, serr)) + assert.False(t, IsOfType(errBadErr, serr)) + assert.True(t, IsOfType(serr, serr)) + + // list error + lerr := AppendError(serr, nil) + assert.False(t, IsOfType(ErrUnknown, lerr)) + assert.False(t, IsOfType(errBadErr, lerr)) + terr = NewTypedError(errBadErr, lerr) + assert.True(t, IsOfType(errBadErr, terr)) +} diff --git a/ethereum/bootstrapper.go b/ethereum/bootstrapper.go index e884e59cc..9bff73d54 100644 --- a/ethereum/bootstrapper.go +++ b/ethereum/bootstrapper.go @@ -1,24 +1,28 @@ package ethereum import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" ) -type Bootstrapper struct { -} +// BootstrappedEthereumClient is a key to mapped client in bootstrap context. +const BootstrappedEthereumClient string = "BootstrappedEthereumClient" + +// Bootstrapper implements bootstrap.Bootstrapper. +type Bootstrapper struct{} -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { +// Bootstrap initialises ethereum client. +func (Bootstrapper) Bootstrap(context map[string]interface{}) error { if _, ok := context[bootstrap.BootstrappedConfig]; !ok { return errors.New("config hasn't been initialized") } - client, err := NewClientConnection(config.Config) + cfg := context[bootstrap.BootstrappedConfig].(Config) + client, err := NewGethClient(cfg) if err != nil { return err } - SetConnection(client) - context[bootstrap.BootstrappedEthereumClient] = client + SetClient(client) + context[BootstrappedEthereumClient] = client return nil } diff --git a/ethereum/geth_client.go b/ethereum/geth_client.go index b20c8f5a8..ffa7dbf70 100644 --- a/ethereum/geth_client.go +++ b/ethereum/geth_client.go @@ -12,43 +12,25 @@ import ( "time" "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" - "github.com/go-errors/errors" logging "github.com/ipfs/go-log" ) -const TransactionUnderpriced = "replacement transaction underpriced" -const NonceTooLow = "nonce too low" +const ( + transactionUnderpriced = errors.Error("replacement transaction underpriced") + nonceTooLow = errors.Error("nonce too low") +) var log = logging.Logger("geth-client") -var gc EthereumClient -var gcInit sync.Once - -// GetDefaultContextTimeout retrieves the default duration before an Ethereum write call context should time out -func GetDefaultContextTimeout() time.Duration { - return config.Config.GetEthereumContextWaitTimeout() -} - -// GetDefaultReadContextTimeout retrieves the default duration before an Ethereum read call context should time out -func GetDefaultReadContextTimeout() time.Duration { - return config.Config.GetEthereumContextReadWaitTimeout() -} - -// DefaultWaitForReadContext returns context with timeout for read operations -func DefaultWaitForReadContext() (ctx context.Context, cancelFunc context.CancelFunc) { - toBeDone := time.Now().Add(GetDefaultReadContextTimeout()) - return context.WithDeadline(context.Background(), toBeDone) -} - -// DefaultWaitForTransactionMiningContext returns context with timeout for write operations -func DefaultWaitForTransactionMiningContext() (ctx context.Context, cancelFunc context.CancelFunc) { - toBeDone := time.Now().Add(GetDefaultContextTimeout()) - return context.WithDeadline(context.TODO(), toBeDone) -} +var gc Client +var gcMu sync.RWMutex +// Config defines functions to get ethereum details type Config interface { GetEthereumGasPrice() *big.Int GetEthereumGasLimit() uint64 @@ -57,219 +39,283 @@ type Config interface { GetEthereumIntervalRetry() time.Duration GetEthereumMaxRetries() int GetTxPoolAccessEnabled() bool + GetEthereumContextReadWaitTimeout() time.Duration } -// Abstract the "ethereum client" out so we can more easily support other clients -// besides Geth (e.g. quorum) -// Also make it easier to mock tests -type EthereumClient interface { - GetClient() *ethclient.Client - GetRpcClient() *rpc.Client - GetHost() *url.URL +// DefaultWaitForTransactionMiningContext returns context with timeout for write operations +func DefaultWaitForTransactionMiningContext(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc) { + toBeDone := time.Now().Add(d) + return context.WithDeadline(context.Background(), toBeDone) +} + +// Client can be implemented by any chain client +type Client interface { + + // GetEthClient returns the ethereum client + GetEthClient() *ethclient.Client + + // GetNodeURL returns the node url + GetNodeURL() *url.URL + + // GetTxOpts returns a cached options if available else creates and returns new options GetTxOpts(accountName string) (*bind.TransactOpts, error) + + // SubmitTransactionWithRetries submits transaction to the ethereum chain + // Blocking Function that sends transaction using reflection wrapped in a retrial block. It is based on the transactionUnderpriced error, + // meaning that a transaction is being attempted to run twice, and the logic is to override the existing one. As we have constant + // gas prices that means that a concurrent transaction race condition event has happened. + // - contractMethod: Contract Method that implements GenericEthereumAsset (usually autogenerated binding from abi) + // - params: Arbitrary number of parameters that are passed to the function fname call + // Note: contractMethod must always return "*types.Transaction, error" SubmitTransactionWithRetries(contractMethod interface{}, opts *bind.TransactOpts, params ...interface{}) (tx *types.Transaction, err error) + + // GetGethCallOpts returns the Call options with default + GetGethCallOpts() (*bind.CallOpts, context.CancelFunc) } -type GethClient struct { - Client *ethclient.Client - RpcClient *rpc.Client - Host *url.URL - Accounts map[string]*bind.TransactOpts - nonceMutex sync.Mutex - config Config +// gethClient implements Client for Ethereum +type gethClient struct { + client *ethclient.Client + rpcClient *rpc.Client + host *url.URL + accounts map[string]*bind.TransactOpts + accMu sync.Mutex // accMu to protect accounts + config Config + + // txMu to ensure one transaction at a time per client + txMu sync.Mutex } -func NewGethClient(config Config) *GethClient { - return &GethClient{ - nil, nil, nil, make(map[string]*bind.TransactOpts), sync.Mutex{}, config, +// NewGethClient returns an gethClient which implements Client +func NewGethClient(config Config) (Client, error) { + log.Info("Opening connection to Ethereum:", config.GetEthereumNodeURL()) + u, err := url.Parse(config.GetEthereumNodeURL()) + if err != nil { + return nil, errors.New("failed to parse ethereum node URL: %v", err) } -} -func (gethClient *GethClient) GetTxOpts(accountName string) (*bind.TransactOpts, error) { - if _, ok := gethClient.Accounts[accountName]; !ok { - txOpts, err := gethClient.getGethTxOpts(accountName) - if err != nil { - return nil, err - } - gethClient.Accounts[accountName] = txOpts + c, err := rpc.Dial(u.String()) + if err != nil { + return nil, errors.New("failed to connect to ethereum node: %v", err) } - gethClient.Accounts[accountName].Nonce = nil // Important to nil the nonce on the cached txopts, otherwise with high concurrency will be outdated - return gethClient.Accounts[accountName], nil + + return &gethClient{ + client: ethclient.NewClient(c), + rpcClient: c, + host: u, + accounts: make(map[string]*bind.TransactOpts), + txMu: sync.Mutex{}, + accMu: sync.Mutex{}, + config: config, + }, nil } -func (gethClient *GethClient) GetClient() *ethclient.Client { - return gethClient.Client +// SetClient sets the Client +// Note that this is a singleton and is the same connection for the whole application. +func SetClient(client Client) { + gcMu.Lock() + defer gcMu.Unlock() + gc = client } -func (gethClient *GethClient) GetRpcClient() *rpc.Client { - return gethClient.RpcClient +// GetClient returns the current Client +func GetClient() Client { + gcMu.RLock() + defer gcMu.RUnlock() + return gc } -func (gethClient *GethClient) GetHost() *url.URL { - return gethClient.Host +// defaultReadContext returns context with timeout for read operations +func (gc *gethClient) defaultReadContext() (ctx context.Context, cancelFunc context.CancelFunc) { + toBeDone := time.Now().Add(gc.config.GetEthereumContextReadWaitTimeout()) + return context.WithDeadline(context.Background(), toBeDone) } -func NewClientConnection(config Config) (*GethClient, error) { - log.Info("Opening connection to Ethereum:", config.GetEthereumNodeURL()) - u, err := url.Parse(config.GetEthereumNodeURL()) - if err != nil { - log.Errorf("Failed to connect to parse ethereum.gethSocket URL: %v", err) - return &GethClient{}, err - } - c, err := rpc.Dial(u.String()) - if err != nil { - log.Errorf("Failed to connect to the Ethereum client [%s]: %v", u.String(), err) - return &GethClient{}, err +// GetTxOpts returns a cached options if available else creates and returns new options +func (gc *gethClient) GetTxOpts(accountName string) (*bind.TransactOpts, error) { + gc.accMu.Lock() + defer gc.accMu.Unlock() + + if opts, ok := gc.accounts[accountName]; ok { + return opts, nil } - client := ethclient.NewClient(c) + + txOpts, err := gc.getGethTxOpts(accountName) if err != nil { - log.Errorf("Failed to connect to the Ethereum client [%s]: %v", u.String(), err) - return &GethClient{}, err + return nil, err } - gethClient := NewGethClient(config) - gethClient.Client = client - gethClient.RpcClient = c - gethClient.Host = u - - return gethClient, nil + gc.accounts[accountName] = txOpts + return txOpts, nil } -// Note that this is a singleton and is the same connection for the whole application. -func SetConnection(conn EthereumClient) { - gcInit.Do(func() { - gc = conn - }) - return +// GetEthClient returns the ethereum client +func (gc *gethClient) GetEthClient() *ethclient.Client { + return gc.client } -// GetConnection returns the connection to the configured `ethereum.gethSocket`. -func GetConnection() EthereumClient { - return gc +// GetNodeURL returns the node url +func (gc *gethClient) GetNodeURL() *url.URL { + return gc.host } // getGethTxOpts retrieves the geth transaction options for the given account name. The account name influences which configuration // is used. -func (gethClient *GethClient) getGethTxOpts(accountName string) (*bind.TransactOpts, error) { - account, err := gethClient.config.GetEthereumAccount(accountName) +func (gc *gethClient) getGethTxOpts(accountName string) (*bind.TransactOpts, error) { + account, err := gc.config.GetEthereumAccount(accountName) if err != nil { - err = errors.Errorf("could not find configured ethereum key for account [%v]. please check your configuration.\n", accountName) - log.Error(err.Error()) - return nil, err + return nil, errors.New("failed to get ethereum account: %v", err) } - authedTransactionOpts, err := bind.NewTransactor(strings.NewReader(account.Key), account.Password) + opts, err := bind.NewTransactor(strings.NewReader(account.Key), account.Password) if err != nil { - err = errors.Errorf("Failed to load key with error: %v", err) - log.Error(err.Error()) - return nil, err - } else { - authedTransactionOpts.GasPrice = gethClient.config.GetEthereumGasPrice() - authedTransactionOpts.GasLimit = gethClient.config.GetEthereumGasLimit() - authedTransactionOpts.Context = context.Background() - return authedTransactionOpts, nil + return nil, errors.New("failed to create new transaction opts: %v", err) } + + opts.GasPrice = gc.config.GetEthereumGasPrice() + opts.GasLimit = gc.config.GetEthereumGasLimit() + opts.Context = context.Background() + return opts, nil } /** -Blocking Function that sends transaction using reflection wrapped in a retrial block. It is based on the TransactionUnderpriced error, +SubmitTransactionWithRetries submits transaction to the ethereum chain +Blocking Function that sends transaction using reflection wrapped in a retrial block. It is based on the transactionUnderpriced error, meaning that a transaction is being attempted to run twice, and the logic is to override the existing one. As we have constant gas prices that means that a concurrent transaction race condition event has happened. - contractMethod: Contract Method that implements GenericEthereumAsset (usually autogenerated binding from abi) - params: Arbitrary number of parameters that are passed to the function fname call +Note: contractMethod must always return "*types.Transaction, error" */ -func (gethClient *GethClient) SubmitTransactionWithRetries(contractMethod interface{}, opts *bind.TransactOpts, params ...interface{}) (tx *types.Transaction, err error) { - done := false - maxTries := gethClient.config.GetEthereumMaxRetries() - current := 0 - var f reflect.Value - var in []reflect.Value - var result []reflect.Value - f = reflect.ValueOf(contractMethod) - - gethClient.nonceMutex.Lock() - defer gethClient.nonceMutex.Unlock() - - for !done { +func (gc *gethClient) SubmitTransactionWithRetries(contractMethod interface{}, opts *bind.TransactOpts, params ...interface{}) (*types.Transaction, error) { + var current int + f := reflect.ValueOf(contractMethod) + maxTries := gc.config.GetEthereumMaxRetries() + + gc.txMu.Lock() + defer gc.txMu.Unlock() + + var err error + for { + if current >= maxTries { - log.Error("Max Concurrent transaction tries reached") - break + return nil, errors.New("max concurrent transaction tries reached: %v", err) } - current += 1 - err = gethClient.incrementNonce(opts) + current++ + err = gc.incrementNonce(opts, gc.config.GetTxPoolAccessEnabled(), gc.client, gc.rpcClient) if err != nil { - return + return nil, errors.New("failed to increment nonce: %v", err) } - in = make([]reflect.Value, len(params)+1) - val := reflect.ValueOf(opts) - in[0] = val - for k, param := range params { - in[k+1] = reflect.ValueOf(param) + + if opts.Nonce != nil { + log.Infof("Incrementing Nonce to [%v]\n", opts.Nonce.String()) + } + + var in []reflect.Value + in = append(in, reflect.ValueOf(opts)) + for _, p := range params { + in = append(in, reflect.ValueOf(p)) + } + + result := f.Call(in) + var tx *types.Transaction + if !result[0].IsNil() { + tx = result[0].Interface().(*types.Transaction) } - result = f.Call(in) - tx = result[0].Interface().(*types.Transaction) - err = nil - if result[1].Interface() != nil { + + if !result[1].IsNil() { err = result[1].Interface().(error) } - if err != nil { - if (err.Error() == TransactionUnderpriced) || (err.Error() == NonceTooLow) { - log.Warningf("Concurrent transaction identified, trying again [%d/%d]\n", current, maxTries) - time.Sleep(gethClient.config.GetEthereumIntervalRetry()) - } else { - done = true - } - } else { - done = true + if err == nil { + return tx, nil + } + + if (err.Error() == transactionUnderpriced.Error()) || (err.Error() == nonceTooLow.Error()) { + log.Warningf("Concurrent transaction identified, trying again [%d/%d]\n", current, maxTries) + time.Sleep(gc.config.GetEthereumIntervalRetry()) + continue } + + return nil, err } - return } -func (gethClient *GethClient) incrementNonce(opts *bind.TransactOpts) (err error) { - if !gethClient.config.GetTxPoolAccessEnabled() { - log.Warningf("Ethereum Client doesn't support txpool API, may cause concurrency issues.") - return +// GetGethCallOpts returns the Call options with default +func (gc *gethClient) GetGethCallOpts() (*bind.CallOpts, context.CancelFunc) { + // Assuring that pending transactions are taken into account by go-ethereum when asking for things like + // specific transactions and client's nonce + // with timeout context, in case eth node is not in sync + ctx, cancel := gc.defaultReadContext() + return &bind.CallOpts{Pending: true, Context: ctx}, cancel +} + +// noncer defines functions to get the next nonce +type noncer interface { + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) +} + +// callContexter defines functions to get CallContext +type callContexter interface { + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error +} + +// incrementNonce updates the opts.Nonce to next valid nonce +// We pick the current nonce by getting latest transactions included in the blocks including pending blocks +// then we check the txpool to see if there any new transactions from the address that are not included in any block +// If there are no pending transactions in txpool, we use the current nonce + 1 +// else we increment the greater of current nonce or the nonce derived from txpool +func (gc *gethClient) incrementNonce(opts *bind.TransactOpts, txpoolAccessEnabled bool, noncer noncer, cc callContexter) error { + // check if the txpool access is enabled + if !txpoolAccessEnabled { + log.Warningf("Ethereum Client doesn't support txpool API, may cause transactions failures") + opts.Nonce = nil + return nil } - var res map[string]map[string]map[string][]string - // Important to not create a DeadLock if network latency - txctx, _ := context.WithTimeout(context.Background(), GetDefaultContextTimeout()) - gc.GetRpcClient().CallContext(txctx, &res, "txpool_inspect") + ctx, cancel := gc.defaultReadContext() + defer cancel() - if len(res["pending"][opts.From.Hex()]) > 0 { - chainNonce, err := gc.GetClient().PendingNonceAt(txctx, opts.From) - if err != nil { - log.Errorf("Error found when getting the Account Chain Nonce [%v]\n", err) - return err - } - CalculateIncrement(chainNonce, res, opts) + // get current nonce + n, err := noncer.PendingNonceAt(ctx, opts.From) + if err != nil { + return errors.New("failed to get chain nonce for %s: %v", opts.From.String(), err) } - return -} + // set the nonce + opts.Nonce = new(big.Int).SetUint64(n) + + // check for any transactions in txpool + res := make(map[string]map[string]map[string]string) + err = cc.CallContext(ctx, &res, "txpool_inspect") + if err != nil { + return errors.New("failed to get txpool data: %v", err) + } + + // no pending transaction from this account in tx pool + if len(res["pending"][opts.From.Hex()]) < 1 { + return nil + } + + var keys []int + for k := range res["pending"][opts.From.Hex()] { + ki, err := strconv.Atoi(k) + if err != nil { + return errors.New("failed to convert nonce: %v", err) + } -func CalculateIncrement(chainNonce uint64, res map[string]map[string]map[string][]string, opts *bind.TransactOpts) { - keys := make([]int, 0, len(res["pending"][opts.From.Hex()])) - for k, _ := range res["pending"][opts.From.Hex()] { - ki, _ := strconv.Atoi(k) keys = append(keys, ki) } + + // there are some pending transactions in txpool, check their nonce + // pick the largest one and increment it sort.Ints(keys) lastPoolNonce := keys[len(keys)-1] - if uint64(lastPoolNonce) >= chainNonce { + if uint64(lastPoolNonce) >= n { opts.Nonce = new(big.Int).Add(big.NewInt(int64(lastPoolNonce)), big.NewInt(1)) - log.Infof("Incrementing Nonce to [%v]\n", opts.Nonce.String()) } -} -func GetGethCallOpts() (*bind.CallOpts, context.CancelFunc) { - // Assuring that pending transactions are taken into account by go-ethereum when asking for things like - // specific transactions and client's nonce - // with timeout context, in case eth node is not in sync - ctx, cancelFunc := DefaultWaitForReadContext() - return &bind.CallOpts{Pending: true, Context: ctx}, cancelFunc + return nil } diff --git a/ethereum/geth_client_integration_test.go b/ethereum/geth_client_integration_test.go index bdc99ca79..27adca307 100644 --- a/ethereum/geth_client_integration_test.go +++ b/ethereum/geth_client_integration_test.go @@ -6,13 +6,18 @@ import ( "os" "testing" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" cc "github.com/centrifuge/go-centrifuge/context/testingbootstrap" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/stretchr/testify/assert" ) +var cfg config.Configuration + func TestMain(m *testing.M) { - cc.TestFunctionalEthereumBootstrap() + ctx := cc.TestFunctionalEthereumBootstrap() + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) result := m.Run() cc.TestFunctionalEthereumTearDown() os.Exit(result) @@ -20,14 +25,20 @@ func TestMain(m *testing.M) { func TestGetConnection_returnsSameConnection(t *testing.T) { howMany := 5 - confChannel := make(chan ethereum.EthereumClient, howMany) + confChannel := make(chan ethereum.Client, howMany) for ix := 0; ix < howMany; ix++ { go func() { - confChannel <- ethereum.GetConnection() + confChannel <- ethereum.GetClient() }() } for ix := 0; ix < howMany; ix++ { multiThreadCreatedCon := <-confChannel - assert.Equal(t, multiThreadCreatedCon, ethereum.GetConnection(), "Should only return a single ethereum client") + assert.Equal(t, multiThreadCreatedCon, ethereum.GetClient(), "Should only return a single ethereum client") } } + +func TestNewGethClient(t *testing.T) { + gc, err := ethereum.NewGethClient(cfg) + assert.Nil(t, err) + assert.NotNil(t, gc) +} diff --git a/ethereum/geth_client_test.go b/ethereum/geth_client_test.go index be2d1fe1d..c7c41edef 100644 --- a/ethereum/geth_client_test.go +++ b/ethereum/geth_client_test.go @@ -1,48 +1,43 @@ // +build unit -package ethereum_test +package ethereum import ( - "math/big" + "context" "os" + "sync" "testing" "time" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/context/testlogging" - "github.com/centrifuge/go-centrifuge/documents/invoice" - "github.com/centrifuge/go-centrifuge/documents/purchaseorder" - "github.com/centrifuge/go-centrifuge/ethereum" - "github.com/centrifuge/go-centrifuge/storage" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/testingutils" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/go-errors/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, - &storage.Bootstrapper{}, - &invoice.Bootstrapper{}, - &purchaseorder.Bootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) - config.Config.V.Set("ethereum.txPoolAccessEnabled", false) - config.Config.V.Set("ethereum.intervalRetry", time.Millisecond*100) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + cfg.Set("ethereum.txPoolAccessEnabled", false) + cfg.Set("ethereum.intervalRetry", time.Millisecond*100) result := m.Run() bootstrap.RunTestTeardown(ibootstappers) os.Exit(result) } -type MockTransactionInterface interface { - RegisterTransaction(opts *bind.TransactOpts, someVar string, anotherVar string) (tx *types.Transaction, err error) -} - type MockTransactionRequest struct { count int } @@ -50,74 +45,154 @@ type MockTransactionRequest struct { func (transactionRequest *MockTransactionRequest) RegisterTransaction(opts *bind.TransactOpts, transactionName string, anotherVar string) (tx *types.Transaction, err error) { transactionRequest.count++ if transactionName == "otherError" { - err = errors.Wrap("Some other error", 1) + err = errors.New("Some other error") } else if transactionName == "optimisticLockingTimeout" { - err = errors.Wrap(ethereum.TransactionUnderpriced, 1) + err = transactionUnderpriced } else if transactionName == "optimisticLockingEventualSuccess" { if transactionRequest.count < 3 { - err = errors.Wrap(ethereum.TransactionUnderpriced, 1) + err = transactionUnderpriced } } - tx = types.NewTransaction(1, common.Address{}, nil, 0, nil, nil) - return + + return types.NewTransaction(1, common.Address{}, nil, 0, nil, nil), err } func TestInitTransactionWithRetries(t *testing.T) { mockRequest := &MockTransactionRequest{} - gc := ethereum.NewGethClient(config.Config) - ethereum.SetConnection(gc) + gc := &gethClient{ + accounts: make(map[string]*bind.TransactOpts), + accMu: sync.Mutex{}, + txMu: sync.Mutex{}, + config: cfg, + } + + SetClient(gc) // Success at first - tx, err := ethereum.GetConnection().SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "var1", "var2") + tx, err := gc.SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "var1", "var2") assert.Nil(t, err, "Should not error out") assert.EqualValues(t, 1, tx.Nonce(), "Nonce should equal to the one provided") assert.EqualValues(t, 1, mockRequest.count, "Transaction Run flag should be true") // Failure with non-locking error - tx, err = ethereum.GetConnection().SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "otherError", "var2") + tx, err = gc.SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "otherError", "var2") assert.EqualError(t, err, "Some other error", "Should error out") - mockRetries := testingutils.MockConfigOption("ethereum.maxRetries", 10) + mockRetries := testingutils.MockConfigOption(cfg, "ethereum.maxRetries", 10) defer mockRetries() mockRequest.count = 0 // Failure and timeout with locking error - tx, err = ethereum.GetConnection().SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "optimisticLockingTimeout", "var2") - assert.EqualError(t, err, ethereum.TransactionUnderpriced, "Should error out") + tx, err = gc.SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "optimisticLockingTimeout", "var2") + assert.Contains(t, err.Error(), transactionUnderpriced, "Should error out") assert.EqualValues(t, 10, mockRequest.count, "Retries should be equal") mockRequest.count = 0 // Success after locking race condition overcome - tx, err = ethereum.GetConnection().SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "optimisticLockingEventualSuccess", "var2") + tx, err = gc.SubmitTransactionWithRetries(mockRequest.RegisterTransaction, &bind.TransactOpts{}, "optimisticLockingEventualSuccess", "var2") assert.Nil(t, err, "Should not error out") assert.EqualValues(t, 3, mockRequest.count, "Retries should be equal") } -func TestCalculateIncrement(t *testing.T) { - strAddress := "0x45B9c4798999FFa52e1ff1eFce9d3e45819E4158" - var input = map[string]map[string]map[string][]string{ - "pending": { - strAddress: { - "0": {"0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas"}, - "1": {"0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas"}, - "2": {"0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas"}, - "3": {"0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas"}, - }, - }, +func TestGetGethCallOpts(t *testing.T) { + opts, cancel := GetClient().GetGethCallOpts() + assert.NotNil(t, opts) + assert.True(t, opts.Pending) + assert.NotNil(t, cancel) + cancel() +} + +type mockNoncer struct { + mock.Mock +} + +func (m *mockNoncer) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + args := m.Called(ctx, account) + n, _ := args.Get(0).(uint64) + return n, args.Error(1) +} + +func (m *mockNoncer) CallContext(ctx context.Context, result interface{}, method string, a ...interface{}) error { + args := m.Called(ctx, result, method, a) + if args.Get(0) != nil { + res := result.(*map[string]map[string]map[string]string) + *res = args.Get(0).(map[string]map[string]map[string]string) } - opts := &bind.TransactOpts{From: common.HexToAddress(strAddress)} + return args.Error(1) +} - // OnChain Transaction Count is behind local tx pool - chainNonce := 3 - expectedNonce := 4 - ethereum.CalculateIncrement(uint64(chainNonce), input, opts) - assert.Equal(t, big.NewInt(int64(expectedNonce)), opts.Nonce, "Nonce should match expected value") +func Test_incrementNonce(t *testing.T) { + opts := &bind.TransactOpts{From: common.HexToAddress("0x45B9c4798999FFa52e1ff1eFce9d3e45819E4158")} + gc := &gethClient{ + config: cfg, + } - // OnChain Transaction Count is ahead of local tx pool, means that txs will get stuck forever (Rare case) - chainNonce = 4 - expectedNonce = 4 - ethereum.CalculateIncrement(uint64(chainNonce), input, opts) - assert.Equal(t, big.NewInt(int64(expectedNonce)), opts.Nonce, "Nonce should match expected value") + // txpool access disabled + err := gc.incrementNonce(opts, false, nil, nil) + assert.Nil(t, err) + assert.Nil(t, opts.Nonce) + + // noncer failed + n := new(mockNoncer) + n.On("PendingNonceAt", mock.Anything, opts.From).Return(0, errors.New("error")).Once() + err = gc.incrementNonce(opts, true, n, nil) + n.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get chain nonce") + + // rpc call failed + n = new(mockNoncer) + n.On("PendingNonceAt", mock.Anything, opts.From).Return(uint64(100), nil).Once() + n.On("CallContext", mock.Anything, mock.Anything, "txpool_inspect", mock.Anything).Return(nil, errors.New("error")).Once() + err = gc.incrementNonce(opts, true, n, n) + n.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get txpool data") + assert.Equal(t, "100", opts.Nonce.String()) + + // no pending txns in the tx pool + n = new(mockNoncer) + n.On("PendingNonceAt", mock.Anything, opts.From).Return(uint64(1000), nil).Once() + n.On("CallContext", mock.Anything, mock.Anything, "txpool_inspect", mock.Anything).Return(nil, nil).Once() + err = gc.incrementNonce(opts, true, n, n) + n.AssertExpectations(t) + assert.Nil(t, err) + assert.Equal(t, "1000", opts.Nonce.String()) + + // bad result + var res = map[string]map[string]map[string]string{ + "pending": { + opts.From.String(): { + "abc": "0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas", + }, + }, + } + n = new(mockNoncer) + n.On("PendingNonceAt", mock.Anything, opts.From).Return(uint64(1000), nil).Once() + n.On("CallContext", mock.Anything, mock.Anything, "txpool_inspect", mock.Anything).Return(res, nil).Once() + err = gc.incrementNonce(opts, true, n, n) + n.AssertExpectations(t) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to convert nonce") + + // higher nonce in txpool + res = map[string]map[string]map[string]string{ + "pending": { + opts.From.String(): { + "1000": "0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas", + "1001": "0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas", + "1002": "0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas", + "1003": "0x958c1fa64b34db746925c6f8a3dd81128e40355e: 1051546810000000000 wei + 90000 × 20000000000 gas", + }, + }, + } + n = new(mockNoncer) + n.On("PendingNonceAt", mock.Anything, opts.From).Return(uint64(1000), nil).Once() + n.On("CallContext", mock.Anything, mock.Anything, "txpool_inspect", mock.Anything).Return(res, nil).Once() + err = gc.incrementNonce(opts, true, n, n) + n.AssertExpectations(t) + assert.Nil(t, err) + assert.Equal(t, "1004", opts.Nonce.String()) } diff --git a/ethereum/test_bootstrapper.go b/ethereum/test_bootstrapper.go index df14ce082..9e9cb981d 100644 --- a/ethereum/test_bootstrapper.go +++ b/ethereum/test_bootstrapper.go @@ -2,10 +2,10 @@ package ethereum -func (b *Bootstrapper) TestBootstrap(context map[string]interface{}) error { +func (b Bootstrapper) TestBootstrap(context map[string]interface{}) error { return b.Bootstrap(context) } -func (*Bootstrapper) TestTearDown() error { +func (Bootstrapper) TestTearDown() error { return nil } diff --git a/header/context.go b/header/context.go new file mode 100644 index 000000000..3e164ee7b --- /dev/null +++ b/header/context.go @@ -0,0 +1,35 @@ +package header + +import ( + "context" + + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" +) + +// ContextHeader holds custom request objects to pass down the pipeline. +type ContextHeader struct { + context context.Context + self *identity.IDConfig +} + +// NewContextHeader creates new instance of the request headers. +func NewContextHeader(context context.Context, config config.Configuration) (*ContextHeader, error) { + idConfig, err := identity.GetIdentityConfig(config.(identity.Config)) + if err != nil { + return nil, errors.New("failed to get id config: %v", err) + } + + return &ContextHeader{self: idConfig, context: context}, nil +} + +// Self returns Self CentID. +func (h *ContextHeader) Self() *identity.IDConfig { + return h.self +} + +// Context returns context.Context of the request. +func (h *ContextHeader) Context() context.Context { + return h.context +} diff --git a/healthcheck/handler.go b/healthcheck/handler.go index 5dcaefa14..b5f96b0a8 100644 --- a/healthcheck/handler.go +++ b/healthcheck/handler.go @@ -3,24 +3,30 @@ package healthcheck import ( "context" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/health" "github.com/centrifuge/go-centrifuge/version" "github.com/golang/protobuf/ptypes/empty" ) +// Config defines methods required for the package healthcheck +type Config interface { + GetNetworkString() string +} + // handler is the grpc handler that implements healthpb.HealthCheckServiceServer -type handler struct{} +type handler struct { + config Config +} // GRPCHandler returns the grpc implementation instance of healthpb.HealthCheckServiceServer -func GRPCHandler() healthpb.HealthCheckServiceServer { - return handler{} +func GRPCHandler(config Config) healthpb.HealthCheckServiceServer { + return handler{config} } // Ping returns the network node is connected to and version of the node -func (handler) Ping(context.Context, *empty.Empty) (pong *healthpb.Pong, err error) { +func (h handler) Ping(context.Context, *empty.Empty) (pong *healthpb.Pong, err error) { return &healthpb.Pong{ Version: version.GetVersion().String(), - Network: config.Config.GetNetworkString(), + Network: h.config.GetNetworkString(), }, nil } diff --git a/healthcheck/handler_test.go b/healthcheck/handler_test.go index 03ff12eb7..b7592fcc9 100644 --- a/healthcheck/handler_test.go +++ b/healthcheck/handler_test.go @@ -4,7 +4,6 @@ package healthcheck import ( "context" - "flag" "os" "testing" @@ -15,23 +14,25 @@ import ( "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &config.Bootstrapper{}, } - - bootstrap.RunTestBootstrappers(ibootstappers, nil) - flag.Parse() + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) result := m.Run() bootstrap.RunTestTeardown(ibootstappers) os.Exit(result) } func TestHandler_Ping(t *testing.T) { - h := GRPCHandler() + h := GRPCHandler(cfg) pong, err := h.Ping(context.Background(), &empty.Empty{}) assert.Nil(t, err) assert.NotNil(t, pong) assert.Equal(t, pong.Version, version.GetVersion().String()) - assert.Equal(t, pong.Network, config.Config.GetNetworkString()) + assert.Equal(t, pong.Network, cfg.GetNetworkString()) } diff --git a/identity/bootstrapper.go b/identity/bootstrapper.go index 132657bd1..b511d56e4 100644 --- a/identity/bootstrapper.go +++ b/identity/bootstrapper.go @@ -1,64 +1,68 @@ package identity import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/queue" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" ) -type Bootstrapper struct { -} +// BootstrappedIDService is used as a key to map the configured ID Service through context. +const BootstrappedIDService string = "BootstrappedIDService" + +// Bootstrapper implements bootstrap.Bootstrapper. +type Bootstrapper struct{} -// Bootstrap initializes the IdentityFactoryContract as well as the IdRegistrationConfirmationTask that depends on it. -// the IdRegistrationConfirmationTask is added to be registered on the Queue at queue.Bootstrapper +// Bootstrap initializes the IdentityFactoryContract as well as the idRegistrationConfirmationTask that depends on it. +// the idRegistrationConfirmationTask is added to be registered on the queue at queue.Bootstrapper func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { if _, ok := context[bootstrap.BootstrappedConfig]; !ok { return errors.New("config hasn't been initialized") } - if _, ok := context[bootstrap.BootstrappedEthereumClient]; !ok { + cfg := context[bootstrap.BootstrappedConfig].(Config) + + if _, ok := context[ethereum.BootstrappedEthereumClient]; !ok { return errors.New("ethereum client hasn't been initialized") } + gethClient := context[ethereum.BootstrappedEthereumClient].(ethereum.Client) - idFactory, err := getIdentityFactoryContract() + idFactory, err := getIdentityFactoryContract(cfg.GetContractAddress("identityFactory"), gethClient) if err != nil { return err } - registryContract, err := getIdentityRegistryContract() + registryContract, err := getIdentityRegistryContract(cfg.GetContractAddress("identityRegistry"), gethClient) if err != nil { return err } - IDService = NewEthereumIdentityService(config.Config, idFactory, registryContract) - - identityContract, err := getIdentityFactoryContract() - if err != nil { - return err + if _, ok := context[bootstrap.BootstrappedQueueServer]; !ok { + return errors.New("queue hasn't been initialized") } + queueSrv := context[bootstrap.BootstrappedQueueServer].(*queue.Server) - err = queue.InstallQueuedTask(context, - NewIdRegistrationConfirmationTask(&identityContract.EthereumIdentityFactoryContractFilterer, ethereum.DefaultWaitForTransactionMiningContext)) - if err != nil { - return err - } + context[BootstrappedIDService] = NewEthereumIdentityService(cfg, idFactory, registryContract, queueSrv, ethereum.GetClient, + func(address common.Address, backend bind.ContractBackend) (contract, error) { + return NewEthereumIdentityContract(address, backend) + }) - err = queue.InstallQueuedTask(context, - NewKeyRegistrationConfirmationTask(ethereum.DefaultWaitForTransactionMiningContext, registryContract, config.Config)) - if err != nil { - return err - } + idRegTask := newIDRegistrationConfirmationTask(cfg.GetEthereumContextWaitTimeout(), &idFactory.EthereumIdentityFactoryContractFilterer, ethereum.DefaultWaitForTransactionMiningContext) + keyRegTask := newKeyRegistrationConfirmationTask(ethereum.DefaultWaitForTransactionMiningContext, registryContract, cfg, queueSrv, ethereum.GetClient, + func(address common.Address, backend bind.ContractBackend) (contract, error) { + return NewEthereumIdentityContract(address, backend) + }) + queueSrv.RegisterTaskType(idRegTask.TaskTypeName(), idRegTask) + queueSrv.RegisterTaskType(keyRegTask.TaskTypeName(), keyRegTask) return nil } -func getIdentityFactoryContract() (identityFactoryContract *EthereumIdentityFactoryContract, err error) { - client := ethereum.GetConnection() - return NewEthereumIdentityFactoryContract(config.Config.GetContractAddress("identityFactory"), client.GetClient()) +func getIdentityFactoryContract(factoryAddress common.Address, ethClient ethereum.Client) (identityFactoryContract *EthereumIdentityFactoryContract, err error) { + return NewEthereumIdentityFactoryContract(factoryAddress, ethClient.GetEthClient()) } -func getIdentityRegistryContract() (identityRegistryContract *EthereumIdentityRegistryContract, err error) { - client := ethereum.GetConnection() - return NewEthereumIdentityRegistryContract(config.Config.GetContractAddress("identityRegistry"), client.GetClient()) +func getIdentityRegistryContract(registryAddress common.Address, ethClient ethereum.Client) (identityRegistryContract *EthereumIdentityRegistryContract, err error) { + return NewEthereumIdentityRegistryContract(registryAddress, ethClient.GetEthClient()) } diff --git a/identity/ethereum_identity.go b/identity/ethereum_identity.go index 2cb32235f..e8c38d68a 100644 --- a/identity/ethereum_identity.go +++ b/identity/ethereum_identity.go @@ -5,104 +5,129 @@ import ( "fmt" "math/big" + "bytes" + + "time" + + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/utils" - "github.com/centrifuge/gocelery" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/go-errors/errors" logging "github.com/ipfs/go-log" ) var log = logging.Logger("identity") -type IdentityFactory interface { - CreateIdentity(opts *bind.TransactOpts, _centrifugeId *big.Int) (*types.Transaction, error) +type factory interface { + CreateIdentity(opts *bind.TransactOpts, centID *big.Int) (*types.Transaction, error) } -type IdentityContract interface { - AddKey(opts *bind.TransactOpts, _key [32]byte, _kPurpose *big.Int) (*types.Transaction, error) +type registry interface { + GetIdentityByCentrifugeId(opts *bind.CallOpts, bigInt *big.Int) (common.Address, error) } -type Config interface { - GetEthereumDefaultAccountName() string +type contract interface { + AddKey(opts *bind.TransactOpts, _key [32]byte, _kPurpose *big.Int) (*types.Transaction, error) + + GetKeysByPurpose(opts *bind.CallOpts, _purpose *big.Int) ([][32]byte, error) + + GetKey(opts *bind.CallOpts, _key [32]byte) (struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }, error) + + FilterKeyAdded(opts *bind.FilterOpts, key [][32]byte, purpose []*big.Int) (*EthereumIdentityContractKeyAddedIterator, error) } +// EthereumIdentityKey holds the identity related details type EthereumIdentityKey struct { Key [32]byte Purposes []*big.Int RevokedAt *big.Int } +// GetKey returns the public key func (idk *EthereumIdentityKey) GetKey() [32]byte { return idk.Key } +// GetPurposes returns the purposes intended for the key func (idk *EthereumIdentityKey) GetPurposes() []*big.Int { return idk.Purposes } +// GetRevokedAt returns the block at which the identity is revoked func (idk *EthereumIdentityKey) GetRevokedAt() *big.Int { return idk.RevokedAt } +// String prints the peerID extracted from the key func (idk *EthereumIdentityKey) String() string { peerID, _ := ed25519.PublicKeyToP2PKey(idk.Key) return fmt.Sprintf("%s", peerID.Pretty()) } -type EthereumIdentity struct { - CentrifugeId CentID - Contract *EthereumIdentityContract - RegistryContract *EthereumIdentityRegistryContract - Config Config -} - -func NewEthereumIdentity(id CentID, registryContract *EthereumIdentityRegistryContract, config Config) *EthereumIdentity { - return &EthereumIdentity{CentrifugeId: id, RegistryContract: registryContract, Config: config} +type ethereumIdentity struct { + centID CentID + contract contract + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error) + registryContract registry + config Config + gethClientFinder func() ethereum.Client + queue *queue.Server } -func (id *EthereumIdentity) CentrifugeID(cenId CentID) { - id.CentrifugeId = cenId +func newEthereumIdentity(id CentID, registryContract registry, config Config, + queue *queue.Server, + gethClientFinder func() ethereum.Client, + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error)) *ethereumIdentity { + return ðereumIdentity{centID: id, registryContract: registryContract, config: config, gethClientFinder: gethClientFinder, contractProvider: contractProvider, queue: queue} } -func (id *EthereumIdentity) CentrifugeIDBytes() CentID { - var idBytes [CentIDLength]byte - copy(idBytes[:], id.CentrifugeId[:CentIDLength]) - return idBytes +// CentrifugeID sets the CentID to the Identity +func (id *ethereumIdentity) SetCentrifugeID(centID CentID) { + id.centID = centID } -func (id *EthereumIdentity) String() string { - return fmt.Sprintf("CentrifugeID [%s]", id.CentrifugeId) +// String returns CentrifugeID +func (id *ethereumIdentity) String() string { + return fmt.Sprintf("CentrifugeID [%s]", id.centID) } -func (id *EthereumIdentity) GetCentrifugeID() CentID { - return id.CentrifugeId +// CentrifugeID returns the CentrifugeID +func (id *ethereumIdentity) CentID() CentID { + return id.centID } -func (id *EthereumIdentity) GetLastKeyForPurpose(keyPurpose int) (key []byte, err error) { +// LastKeyForPurpose returns the latest key for given purpose +func (id *ethereumIdentity) LastKeyForPurpose(keyPurpose int) (key []byte, err error) { idKeys, err := id.fetchKeysByPurpose(keyPurpose) if err != nil { return []byte{}, err } if len(idKeys) == 0 { - return []byte{}, fmt.Errorf("no key found for type [%d] in ID [%s]", keyPurpose, id.CentrifugeId) + return []byte{}, errors.New("no key found for type [%d] in ID [%s]", keyPurpose, id.centID) } return idKeys[len(idKeys)-1].Key[:32], nil } -func (id *EthereumIdentity) FetchKey(key []byte) (Key, error) { +// FetchKey fetches the Key from the chain +func (id *ethereumIdentity) FetchKey(key []byte) (Key, error) { contract, err := id.getContract() if err != nil { return nil, err } // Ignoring cancelFunc as code will block until response or timeout is triggered - opts, _ := ethereum.GetGethCallOpts() + opts, _ := id.gethClientFinder().GetGethCallOpts() key32, _ := utils.SliceToByte32(key) keyInstance, err := contract.GetKey(opts, key32) if err != nil { @@ -117,28 +142,30 @@ func (id *EthereumIdentity) FetchKey(key []byte) (Key, error) { } -func (id *EthereumIdentity) GetCurrentP2PKey() (ret string, err error) { - key, err := id.GetLastKeyForPurpose(KeyPurposeP2p) +// CurrentP2PKey returns the latest P2P key +func (id *ethereumIdentity) CurrentP2PKey() (ret string, err error) { + key, err := id.LastKeyForPurpose(KeyPurposeP2P) if err != nil { - return + return ret, err } key32, _ := utils.SliceToByte32(key) - p2pId, err := ed25519.PublicKeyToP2PKey(key32) + p2pID, err := ed25519.PublicKeyToP2PKey(key32) if err != nil { - return + return ret, err } - ret = p2pId.Pretty() - return + + return p2pID.Pretty(), nil } -func (id *EthereumIdentity) findContract() (exists bool, err error) { - if id.Contract != nil { +func (id *ethereumIdentity) findContract() (exists bool, err error) { + if id.contract != nil { return true, nil } + client := id.gethClientFinder() // Ignoring cancelFunc as code will block until response or timeout is triggered - opts, _ := ethereum.GetGethCallOpts() - idAddress, err := id.RegistryContract.GetIdentityByCentrifugeId(opts, id.CentrifugeId.BigInt()) + opts, _ := client.GetGethCallOpts() + idAddress, err := id.registryContract.GetIdentityByCentrifugeId(opts, id.centID.BigInt()) if err != nil { return false, err } @@ -146,8 +173,7 @@ func (id *EthereumIdentity) findContract() (exists bool, err error) { return false, errors.New("Identity not found by address provided") } - client := ethereum.GetConnection() - idContract, err := NewEthereumIdentityContract(idAddress, client.GetClient()) + idContract, err := id.contractProvider(idAddress, client.GetEthClient()) if err == bind.ErrNoCode { return false, err } @@ -155,27 +181,23 @@ func (id *EthereumIdentity) findContract() (exists bool, err error) { log.Errorf("Failed to instantiate the identity contract: %v", err) return false, err } - id.Contract = idContract + id.contract = idContract return true, nil } -func (id *EthereumIdentity) getContract() (contract *EthereumIdentityContract, err error) { - if id.Contract == nil { +func (id *ethereumIdentity) getContract() (contract contract, err error) { + if id.contract == nil { _, err := id.findContract() if err != nil { return nil, err } - return id.Contract, nil + return id.contract, nil } - return id.Contract, nil + return id.contract, nil } -// CheckIdentityExists checks if the identity represented by id actually exists on ethereum -func (id *EthereumIdentity) CheckIdentityExists() (exists bool, err error) { - return id.findContract() -} - -func (id *EthereumIdentity) AddKeyToIdentity(ctx context.Context, keyPurpose int, key []byte) (confirmations chan *WatchIdentity, err error) { +// AddKeyToIdentity adds key to the purpose on chain +func (id *ethereumIdentity) AddKeyToIdentity(ctx context.Context, keyPurpose int, key []byte) (confirmations chan *WatchIdentity, err error) { if utils.IsEmptyByteSlice(key) || len(key) > 32 { log.Errorf("Can't add key to identity: empty or invalid length(>32) key for [id: %s]: %x", id, key) return confirmations, errors.New("Can't add key to identity: Invalid key") @@ -187,42 +209,43 @@ func (id *EthereumIdentity) AddKeyToIdentity(ctx context.Context, keyPurpose int return confirmations, err } - conn := ethereum.GetConnection() - opts, err := ethereum.GetConnection().GetTxOpts(id.Config.GetEthereumDefaultAccountName()) + conn := id.gethClientFinder() + opts, err := ethereum.GetClient().GetTxOpts(id.config.GetEthereumDefaultAccountName()) if err != nil { return confirmations, err } - h, err := conn.GetClient().HeaderByNumber(ctx, nil) + h, err := conn.GetEthClient().HeaderByNumber(ctx, nil) if err != nil { return confirmations, err } var keyFixed [32]byte copy(keyFixed[:], key) - confirmations, err = setUpKeyRegisteredEventListener(id, keyPurpose, keyFixed, h.Number.Uint64()) + confirmations, err = id.setUpKeyRegisteredEventListener(id.config, id, keyPurpose, keyFixed, h.Number.Uint64()) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Errorf("Failed to set up event listener for identity [id: %s]: %v", id, wError) return confirmations, wError } err = sendKeyRegistrationTransaction(ethIdentityContract, opts, id, keyPurpose, key) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Errorf("Failed to create transaction for identity [id: %s]: %v", id, wError) return confirmations, wError } return confirmations, nil } -func (id *EthereumIdentity) fetchKeysByPurpose(keyPurpose int) ([]EthereumIdentityKey, error) { +// fetchKeysByPurpose fetches keys from chain matching purpose +func (id *ethereumIdentity) fetchKeysByPurpose(keyPurpose int) ([]EthereumIdentityKey, error) { contract, err := id.getContract() if err != nil { return nil, err } // Ignoring cancelFunc as code will block until response or timeout is triggered - opts, _ := ethereum.GetGethCallOpts() + opts, _ := id.gethClientFinder().GetGethCallOpts() bigInt := big.NewInt(int64(keyPurpose)) keys, err := contract.GetKeysByPurpose(opts, bigInt) if err != nil { @@ -239,7 +262,7 @@ func (id *EthereumIdentity) fetchKeysByPurpose(keyPurpose int) ([]EthereumIdenti } // sendRegistrationTransaction sends the actual transaction to add a Key on Ethereum registry contract -func sendKeyRegistrationTransaction(identityContract IdentityContract, opts *bind.TransactOpts, identity *EthereumIdentity, keyPurpose int, key []byte) (err error) { +func sendKeyRegistrationTransaction(identityContract contract, opts *bind.TransactOpts, identity *ethereumIdentity, keyPurpose int, key []byte) error { //preparation of data in specific types for the call to Ethereum bigInt := big.NewInt(int64(keyPurpose)) @@ -248,125 +271,147 @@ func sendKeyRegistrationTransaction(identityContract IdentityContract, opts *bin return err } - tx, err := ethereum.GetConnection().SubmitTransactionWithRetries(identityContract.AddKey, opts, bKey, bigInt) + tx, err := ethereum.GetClient().SubmitTransactionWithRetries(identityContract.AddKey, opts, bKey, bigInt) if err != nil { - log.Infof("Failed to send key [%v:%x] to add to CentrifugeID [%x]: %v", keyPurpose, bKey, identity.CentrifugeId, err) + log.Infof("Failed to send key [%v:%x] to add to CentrifugeID [%x]: %v", keyPurpose, bKey, identity.CentID, err) return err } log.Infof("Sent off key [%v:%x] to add to CentrifugeID [%s]. Ethereum transaction hash [%x]", keyPurpose, bKey, identity, tx.Hash()) - return + return nil } // sendIdentityCreationTransaction sends the actual transaction to create identity on Ethereum registry contract -func sendIdentityCreationTransaction(identityFactory IdentityFactory, opts *bind.TransactOpts, identityToBeCreated Identity) (err error) { +func sendIdentityCreationTransaction(identityFactory factory, opts *bind.TransactOpts, identityToBeCreated Identity) error { //preparation of data in specific types for the call to Ethereum - tx, err := ethereum.GetConnection().SubmitTransactionWithRetries(identityFactory.CreateIdentity, opts, identityToBeCreated.GetCentrifugeID().BigInt()) - + tx, err := ethereum.GetClient().SubmitTransactionWithRetries(identityFactory.CreateIdentity, opts, identityToBeCreated.CentID().BigInt()) if err != nil { log.Infof("Failed to send identity for creation [CentrifugeID: %s] : %v", identityToBeCreated, err) return err - } else { - log.Infof("Sent off identity creation [%s]. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", identityToBeCreated, tx.Hash(), tx.Nonce(), tx.CheckNonce()) } + log.Infof("Sent off identity creation [%s]. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", identityToBeCreated, tx.Hash(), tx.Nonce(), tx.CheckNonce()) log.Infof("Transfer pending: 0x%x\n", tx.Hash()) - - return + return err } -func setUpKeyRegisteredEventListener(identity Identity, keyPurpose int, key [32]byte, blockHeight uint64) (confirmations chan *WatchIdentity, err error) { +// setUpKeyRegisteredEventListener listens for Identity creation +func (id *ethereumIdentity) setUpKeyRegisteredEventListener(config Config, identity Identity, keyPurpose int, key [32]byte, bh uint64) (confirmations chan *WatchIdentity, err error) { confirmations = make(chan *WatchIdentity) - centId := identity.GetCentrifugeID() + centID := identity.CentID() if err != nil { return nil, err } - asyncRes, err := queue.Queue.DelayKwargs(KeyRegistrationConfirmationTaskName, + asyncRes, err := id.queue.EnqueueJob(keyRegistrationConfirmationTaskName, map[string]interface{}{ - CentIdParam: centId, - KeyParam: key, - KeyPurposeParam: keyPurpose, - BlockHeight: blockHeight, + centIDParam: centID, + keyParam: key, + keyPurposeParam: keyPurpose, + queue.BlockHeightParam: bh, }) if err != nil { return nil, err } - go waitAndRouteKeyRegistrationEvent(asyncRes, confirmations, identity) + go waitAndRouteKeyRegistrationEvent(config.GetEthereumContextWaitTimeout(), asyncRes, confirmations, identity) return confirmations, nil } // setUpRegistrationEventListener sets up the listened for the "IdentityCreated" event to notify the upstream code about successful mining/creation // of the identity. -func setUpRegistrationEventListener(identityToBeCreated Identity, blockHeight uint64) (confirmations chan *WatchIdentity, err error) { +func (ids *EthereumIdentityService) setUpRegistrationEventListener(config Config, identityToBeCreated Identity, blockHeight uint64) (confirmations chan *WatchIdentity, err error) { confirmations = make(chan *WatchIdentity) - bCentId := identityToBeCreated.GetCentrifugeID() + centID := identityToBeCreated.CentID() if err != nil { return nil, err } - asyncRes, err := queue.Queue.DelayKwargs(IdRegistrationConfirmationTaskName, map[string]interface{}{CentIdParam: bCentId, BlockHeight: blockHeight}) + + asyncRes, err := ids.queue.EnqueueJob(idRegistrationConfirmationTaskName, map[string]interface{}{centIDParam: centID, queue.BlockHeightParam: blockHeight}) if err != nil { return nil, err } - go waitAndRouteIdentityRegistrationEvent(asyncRes, confirmations, identityToBeCreated) + go waitAndRouteIdentityRegistrationEvent(config.GetEthereumContextWaitTimeout(), asyncRes, confirmations, identityToBeCreated) return confirmations, nil } // waitAndRouteKeyRegistrationEvent notifies the confirmations channel whenever the key has been added to the identity and has been noted as Ethereum event -func waitAndRouteKeyRegistrationEvent(asyncRes *gocelery.AsyncResult, confirmations chan<- *WatchIdentity, pushThisIdentity Identity) { - _, err := asyncRes.Get(ethereum.GetDefaultContextTimeout()) +func waitAndRouteKeyRegistrationEvent(timeout time.Duration, asyncRes queue.TaskResult, confirmations chan<- *WatchIdentity, pushThisIdentity Identity) { + _, err := asyncRes.Get(timeout) confirmations <- &WatchIdentity{Identity: pushThisIdentity, Error: err} } // waitAndRouteIdentityRegistrationEvent notifies the confirmations channel whenever the identity creation is being noted as Ethereum event -func waitAndRouteIdentityRegistrationEvent(asyncRes *gocelery.AsyncResult, confirmations chan<- *WatchIdentity, pushThisIdentity Identity) { - _, err := asyncRes.Get(ethereum.GetDefaultContextTimeout()) +func waitAndRouteIdentityRegistrationEvent(timeout time.Duration, asyncRes queue.TaskResult, confirmations chan<- *WatchIdentity, pushThisIdentity Identity) { + _, err := asyncRes.Get(timeout) confirmations <- &WatchIdentity{pushThisIdentity, err} } -// EthereumidentityService implements `Service` +// EthereumIdentityService implements `Service` type EthereumIdentityService struct { config Config - factoryContract *EthereumIdentityFactoryContract - registryContract *EthereumIdentityRegistryContract + factoryContract factory + registryContract registry + gethClientFinder func() ethereum.Client + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error) + queue *queue.Server } // NewEthereumIdentityService creates a new NewEthereumIdentityService given the config and the smart contracts -func NewEthereumIdentityService(config Config, factoryContract *EthereumIdentityFactoryContract, registryContract *EthereumIdentityRegistryContract) Service { - return &EthereumIdentityService{config: config, factoryContract: factoryContract, registryContract: registryContract} +func NewEthereumIdentityService(config Config, factoryContract factory, registryContract registry, + queue *queue.Server, + gethClientFinder func() ethereum.Client, + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error)) Service { + return &EthereumIdentityService{config: config, factoryContract: factoryContract, registryContract: registryContract, gethClientFinder: gethClientFinder, contractProvider: contractProvider, queue: queue} } // CheckIdentityExists checks if the identity represented by id actually exists on ethereum func (ids *EthereumIdentityService) CheckIdentityExists(centrifugeID CentID) (exists bool, err error) { - id := NewEthereumIdentity(centrifugeID, ids.registryContract, ids.config) - exists, err = id.CheckIdentityExists() - return + client := ids.gethClientFinder() + // Ignoring cancelFunc as code will block until response or timeout is triggered + opts, _ := client.GetGethCallOpts() + idAddress, err := ids.registryContract.GetIdentityByCentrifugeId(opts, centrifugeID.BigInt()) + if err != nil { + return false, err + } + if utils.IsEmptyByteSlice(idAddress.Bytes()) { + return false, errors.New("Identity not found by address provided") + } + + _, err = NewEthereumIdentityContract(idAddress, client.GetEthClient()) + if err == bind.ErrNoCode { + return false, err + } + if err != nil { + log.Errorf("Failed to instantiate the identity contract: %v", err) + return false, err + } + return true, nil } // CreateIdentity creates an identity representing the id on ethereum func (ids *EthereumIdentityService) CreateIdentity(centrifugeID CentID) (id Identity, confirmations chan *WatchIdentity, err error) { - log.Infof("Creating Identity [%x]", centrifugeID.ByteArray()) - id = NewEthereumIdentity(centrifugeID, ids.registryContract, ids.config) - conn := ethereum.GetConnection() + log.Infof("Creating Identity [%x]", centrifugeID) + id = newEthereumIdentity(centrifugeID, ids.registryContract, ids.config, ids.queue, ids.gethClientFinder, ids.contractProvider) + conn := ids.gethClientFinder() opts, err := conn.GetTxOpts(ids.config.GetEthereumDefaultAccountName()) if err != nil { return nil, confirmations, err } - h, err := conn.GetClient().HeaderByNumber(context.Background(), nil) + h, err := conn.GetEthClient().HeaderByNumber(context.Background(), nil) if err != nil { return nil, confirmations, err } - confirmations, err = setUpRegistrationEventListener(id, h.Number.Uint64()) + confirmations, err = ids.setUpRegistrationEventListener(ids.config, id, h.Number.Uint64()) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Infof("Failed to set up event listener for identity [mockID: %s]: %v", id, wError) return nil, confirmations, wError } err = sendIdentityCreationTransaction(ids.factoryContract, opts, id) if err != nil { - wError := errors.Wrap(err, 1) + wError := errors.New("%v", err) log.Infof("Failed to create transaction for identity [mockID: %s]: %v", id, wError) return nil, confirmations, wError } @@ -376,7 +421,7 @@ func (ids *EthereumIdentityService) CreateIdentity(centrifugeID CentID) (id Iden // GetIdentityAddress gets the address of the ethereum identity contract for the given CentID func (ids *EthereumIdentityService) GetIdentityAddress(centID CentID) (common.Address, error) { // Ignoring cancelFunc as code will block until response or timeout is triggered - opts, _ := ethereum.GetGethCallOpts() + opts, _ := ethereum.GetClient().GetGethCallOpts() address, err := ids.registryContract.GetIdentityByCentrifugeId(opts, centID.BigInt()) if err != nil { return common.Address{}, err @@ -390,15 +435,130 @@ func (ids *EthereumIdentityService) GetIdentityAddress(centID CentID) (common.Ad // LookupIdentityForID looks up if the identity for given CentID exists on ethereum func (ids *EthereumIdentityService) LookupIdentityForID(centrifugeID CentID) (Identity, error) { - id := NewEthereumIdentity(centrifugeID, ids.registryContract, ids.config) - exists, err := id.CheckIdentityExists() + exists, err := ids.CheckIdentityExists(centrifugeID) if !exists { - return id, fmt.Errorf("identity [%s] does not exist with err [%v]", id.CentrifugeId, err) + return nil, errors.New("identity [%s] does not exist with err [%v]", centrifugeID, err) + } + + if err != nil { + return nil, err } + return newEthereumIdentity(centrifugeID, ids.registryContract, ids.config, ids.queue, ids.gethClientFinder, ids.contractProvider), nil +} +// GetClientP2PURL returns the p2p url associated with the centID +func (ids *EthereumIdentityService) GetClientP2PURL(centID CentID) (url string, err error) { + target, err := ids.LookupIdentityForID(centID) if err != nil { - return id, err + return url, errors.New("error fetching receiver identity: %v", err) + } + + p2pKey, err := target.CurrentP2PKey() + if err != nil { + return url, errors.New("error fetching p2p key: %v", err) + } + + return fmt.Sprintf("/ipfs/%s", p2pKey), nil +} + +// GetClientsP2PURLs returns p2p urls associated with each centIDs +// will error out at first failure +func (ids *EthereumIdentityService) GetClientsP2PURLs(centIDs []CentID) ([]string, error) { + var p2pURLs []string + for _, id := range centIDs { + url, err := ids.GetClientP2PURL(id) + if err != nil { + return nil, err + } + + p2pURLs = append(p2pURLs, url) + } + + return p2pURLs, nil +} + +// GetIdentityKey returns the key for provided identity +func (ids *EthereumIdentityService) GetIdentityKey(identity CentID, pubKey []byte) (keyInfo Key, err error) { + id, err := ids.LookupIdentityForID(identity) + if err != nil { + return keyInfo, err + } + + key, err := id.FetchKey(pubKey) + if err != nil { + return keyInfo, err + } + + if utils.IsEmptyByte32(key.GetKey()) { + return keyInfo, errors.New("key not found for identity: %x", identity) + } + + return key, nil +} + +// ValidateKey checks if a given key is valid for the given centrifugeID. +func (ids *EthereumIdentityService) ValidateKey(centID CentID, key []byte, purpose int) error { + idKey, err := ids.GetIdentityKey(centID, key) + if err != nil { + return err + } + + if !bytes.Equal(key, utils.Byte32ToSlice(idKey.GetKey())) { + return errors.New("[Key: %x] Key doesn't match", idKey.GetKey()) + } + + if !utils.ContainsBigIntInSlice(big.NewInt(int64(purpose)), idKey.GetPurposes()) { + return errors.New("[Key: %x] Key doesn't have purpose [%d]", idKey.GetKey(), purpose) + } + + if idKey.GetRevokedAt().Cmp(big.NewInt(0)) != 0 { + return errors.New("[Key: %x] Key is currently revoked since block [%d]", idKey.GetKey(), idKey.GetRevokedAt()) + } + + return nil +} + +// AddKeyFromConfig adds a key previously generated and indexed in the configuration file to the identity specified in such config file +func (ids *EthereumIdentityService) AddKeyFromConfig(purpose int) error { + identityConfig, err := GetIdentityConfig(ids.config) + if err != nil { + return err + } + + id, err := ids.LookupIdentityForID(identityConfig.ID) + if err != nil { + return err + } + + ctx, cancel := ethereum.DefaultWaitForTransactionMiningContext(ids.config.GetEthereumContextWaitTimeout()) + defer cancel() + confirmations, err := id.AddKeyToIdentity(ctx, purpose, identityConfig.Keys[purpose].PublicKey) + if err != nil { + return err + } + watchAddedToIdentity := <-confirmations + + lastKey, errLocal := watchAddedToIdentity.Identity.LastKeyForPurpose(purpose) + if errLocal != nil { + return err + } + + log.Infof("Key [%v] with type [%d] Added to Identity [%s]", lastKey, purpose, watchAddedToIdentity.Identity) + + return nil +} + +// ValidateSignature validates a signature on a message based on identity data +func (ids *EthereumIdentityService) ValidateSignature(signature *coredocumentpb.Signature, message []byte) error { + centID, err := ToCentID(signature.EntityId) + if err != nil { + return err + } + + err = ids.ValidateKey(centID, signature.PublicKey, KeyPurposeSigning) + if err != nil { + return err } - return id, nil + return signatures.VerifySignature(signature.PublicKey, message, signature.Signature) } diff --git a/identity/ethereum_identity_integration_test.go b/identity/ethereum_identity_integration_test.go index d7d826bce..688792879 100644 --- a/identity/ethereum_identity_integration_test.go +++ b/identity/ethereum_identity_integration_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" cc "github.com/centrifuge/go-centrifuge/context/testingbootstrap" "github.com/centrifuge/go-centrifuge/identity" @@ -17,16 +18,18 @@ import ( ) var identityService identity.Service +var cfg config.Configuration func TestMain(m *testing.M) { // Adding delay to startup (concurrency hack) time.Sleep(time.Second + 2) - cc.TestFunctionalEthereumBootstrap() - config.Config.V.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") - config.Config.V.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + ctx := cc.TestFunctionalEthereumBootstrap() + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") - identityService = identity.IDService + identityService = ctx[identity.BootstrappedIDService].(identity.Service) result := m.Run() cc.TestFunctionalEthereumTearDown() os.Exit(result) @@ -46,12 +49,12 @@ func TestCreateAndLookupIdentity_Integration(t *testing.T) { watchRegisteredIdentity := <-confirmations assert.Nil(t, watchRegisteredIdentity.Error, "No error thrown by context") - assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") // LookupIdentityForID id, err = identityService.LookupIdentityForID(centrifugeId) assert.Nil(t, err, "should not error out when resolving identity") - assert.Equal(t, centrifugeId, id.GetCentrifugeID(), "CentrifugeID Should match provided one") + assert.Equal(t, centrifugeId, id.CentID(), "CentrifugeID Should match provided one") _, err = identityService.LookupIdentityForID(wrongCentrifugeIdTyped) assert.NotNil(t, err, "should error out when resolving wrong identity") @@ -66,47 +69,47 @@ func TestCreateAndLookupIdentity_Integration(t *testing.T) { assert.Nil(t, err, "should not error out when adding key to identity") assert.NotNil(t, confirmations, "confirmations channel should not be nil") watchReceivedIdentity := <-confirmations - assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") - recKey, err := id.GetLastKeyForPurpose(1) + recKey, err := id.LastKeyForPurpose(1) assert.Nil(t, err) assert.Equal(t, key, recKey) - _, err = id.GetLastKeyForPurpose(2) + _, err = id.LastKeyForPurpose(2) assert.NotNil(t, err) } func TestAddKeyFromConfig(t *testing.T) { centrifugeId, _ := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) - defaultCentrifugeId := config.Config.V.GetString("identityId") - config.Config.V.Set("identityId", centrifugeId.String()) - config.Config.V.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") - config.Config.V.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + defaultCentrifugeId := cfg.GetString("identityId") + cfg.Set("identityId", centrifugeId.String()) + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") _, confirmations, err := identityService.CreateIdentity(centrifugeId) assert.Nil(t, err, "should not error out when creating identity") watchRegisteredIdentity := <-confirmations assert.Nil(t, watchRegisteredIdentity.Error, "No error thrown by context") - assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") - err = identity.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) + err = identityService.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) assert.Nil(t, err, "should not error out") - config.Config.V.Set("identityId", defaultCentrifugeId) + cfg.Set("identityId", defaultCentrifugeId) } func TestAddKeyFromConfig_IdentityDoesNotExist(t *testing.T) { centrifugeId, _ := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) - defaultCentrifugeId := config.Config.V.GetString("identityId") - config.Config.V.Set("identityId", centrifugeId.String()) - config.Config.V.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") - config.Config.V.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + defaultCentrifugeId := cfg.GetString("identityId") + cfg.Set("identityId", centrifugeId.String()) + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") - err := identity.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) + err := identityService.AddKeyFromConfig(identity.KeyPurposeEthMsgAuth) assert.NotNil(t, err, "should error out") - config.Config.V.Set("identityId", defaultCentrifugeId) + cfg.Set("identityId", defaultCentrifugeId) } func TestCreateAndLookupIdentity_Integration_Concurrent(t *testing.T) { @@ -122,9 +125,9 @@ func TestCreateAndLookupIdentity_Integration_Concurrent(t *testing.T) { for ix := 0; ix < 5; ix++ { watchSingleIdentity := <-identityConfirmations[ix] - id, err := identityService.LookupIdentityForID(watchSingleIdentity.Identity.GetCentrifugeID()) + id, err := identityService.LookupIdentityForID(watchSingleIdentity.Identity.CentID()) assert.Nil(t, err, "should not error out upon identity resolution") - assert.Equal(t, centIds[ix], id.GetCentrifugeID(), "Should have the ID that was passed into create function [%v]", id.GetCentrifugeID()) + assert.Equal(t, centIds[ix], id.CentID(), "Should have the ID that was passed into create function [%v]", id.CentID()) } } @@ -139,6 +142,6 @@ func TestEthereumIdentityService_GetIdentityAddress(t *testing.T) { } func TestEthereumIdentityService_GetIdentityAddressNonExistingID(t *testing.T) { - _, err := identityService.GetIdentityAddress(identity.NewRandomCentID()) + _, err := identityService.GetIdentityAddress(identity.RandomCentID()) assert.NotNil(t, err) } diff --git a/identity/ethereum_identity_test.go b/identity/ethereum_identity_test.go new file mode 100644 index 000000000..8388a36f8 --- /dev/null +++ b/identity/ethereum_identity_test.go @@ -0,0 +1,450 @@ +// +build unit + +package identity + +import ( + "context" + "math/big" + "net/url" + "testing" + + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockIDFactory struct { + mock.Mock +} + +func (f MockIDFactory) CreateIdentity(opts *bind.TransactOpts, _centrifugeId *big.Int) (*types.Transaction, error) { + args := f.Called(opts, _centrifugeId) + id := args.Get(0).(*types.Transaction) + return id, args.Error(1) +} + +type MockIDRegistry struct { + mock.Mock +} + +func (r MockIDRegistry) GetIdentityByCentrifugeId(opts *bind.CallOpts, bigInt *big.Int) (common.Address, error) { + args := r.Called(opts, bigInt) + id := args.Get(0).(common.Address) + return id, args.Error(1) +} + +type MockGethClient struct { + mock.Mock +} + +func (gc MockGethClient) GetEthClient() *ethclient.Client { + args := gc.Called() + return args.Get(0).(*ethclient.Client) +} + +func (gc MockGethClient) GetNodeURL() *url.URL { + args := gc.Called() + return args.Get(0).(*url.URL) +} + +func (gc MockGethClient) GetTxOpts(accountName string) (*bind.TransactOpts, error) { + args := gc.Called(accountName) + return args.Get(0).(*bind.TransactOpts), args.Error(1) +} + +func (gc MockGethClient) SubmitTransactionWithRetries(contractMethod interface{}, opts *bind.TransactOpts, params ...interface{}) (tx *types.Transaction, err error) { + args := gc.Called(contractMethod, opts, params) + return args.Get(0).(*types.Transaction), args.Error(1) +} + +func (gc MockGethClient) GetGethCallOpts() (*bind.CallOpts, context.CancelFunc) { + args := gc.Called() + return args.Get(0).(*bind.CallOpts), args.Get(1).(func()) +} + +type MockIDContract struct { + mock.Mock +} + +func (mic MockIDContract) AddKey(opts *bind.TransactOpts, _key [32]byte, _kPurpose *big.Int) (*types.Transaction, error) { + args := mic.Called(opts, _key, _kPurpose) + return args.Get(0).(*types.Transaction), args.Error(1) +} + +func (mic MockIDContract) GetKeysByPurpose(opts *bind.CallOpts, _purpose *big.Int) ([][32]byte, error) { + args := mic.Called(opts, _purpose) + return args.Get(0).([][32]byte), args.Error(1) +} + +func (mic MockIDContract) GetKey(opts *bind.CallOpts, _key [32]byte) (struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int +}, error) { + args := mic.Called(opts, _key) + return args.Get(0).(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }), args.Error(1) +} + +func (mic MockIDContract) FilterKeyAdded(opts *bind.FilterOpts, key [][32]byte, purpose []*big.Int) (*EthereumIdentityContractKeyAddedIterator, error) { + args := mic.Called(opts, key, purpose) + return args.Get(0).(*EthereumIdentityContractKeyAddedIterator), args.Error(1) +} + +func TestGetClientP2PURL_happy(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKeysByPurpose", mock.Anything, mock.Anything).Return([][32]byte{{1}}, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetClientP2PURL(centID) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.NotEmpty(t, p2p, "p2p url is empty") + assert.Nil(t, err, "error should be nil") +} + +func TestGetClientP2PURL_fail_identity_lookup(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), errors.New("ID lookup failed")) + i.On("GetKeysByPurpose", mock.Anything, mock.Anything).Return([][32]byte{{1}}, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetClientP2PURL(centID) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Empty(t, p2p, "p2p is not empty") + assert.Errorf(t, err, "error should not be nil") + assert.Contains(t, err.Error(), "ID lookup failed") +} + +func TestGetClientP2PURL_fail_p2pkey_error(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKeysByPurpose", mock.Anything, mock.Anything).Return([][32]byte{{1}}, errors.New("p2p key error")) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetClientP2PURL(centID) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Empty(t, p2p, "p2p is not empty") + assert.Errorf(t, err, "error should not be nil") + assert.Contains(t, err.Error(), "p2p key error") +} + +func TestGetIdentityKey_fail_lookup(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), errors.New("ID lookup failed")) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetIdentityKey(centID, pubKey) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Empty(t, p2p, "p2p is not empty") + assert.Errorf(t, err, "error should not be nil") + assert.Contains(t, err.Error(), "ID lookup failed") +} + +func TestGetIdentityKey_fail_FetchKey(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + [32]byte{1}, + []*big.Int{big.NewInt(KeyPurposeP2P)}, + big.NewInt(1), + }, errors.New("p2p key error")) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetIdentityKey(centID, pubKey) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Empty(t, p2p, "p2p is not empty") + assert.Errorf(t, err, "error should not be nil") + assert.Contains(t, err.Error(), "p2p key error") +} + +func TestGetIdentityKey_fail_empty(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + [32]byte{}, + []*big.Int{big.NewInt(KeyPurposeP2P)}, + big.NewInt(1), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetIdentityKey(centID, pubKey) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Empty(t, p2p, "p2p is not empty") + assert.Errorf(t, err, "error should not be nil") + assert.Contains(t, err.Error(), "key not found for identity") +} + +func TestGetIdentityKey_Success(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + [32]byte{1}, + []*big.Int{big.NewInt(KeyPurposeP2P)}, + big.NewInt(1), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + p2p, err := srv.GetIdentityKey(centID, pubKey) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.NotEmpty(t, p2p, "p2p is empty") + assert.Nil(t, err, "error must be nil") +} + +func TestValidateKey_success(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + var key [32]byte + copy(key[:], pubKey) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + key, + []*big.Int{big.NewInt(KeyPurposeSigning)}, + big.NewInt(0), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Nil(t, err, "error must be nil") +} + +func TestValidateKey_fail_getId(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + var key [32]byte + copy(key[:], pubKey) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + key, + []*big.Int{big.NewInt(KeyPurposeSigning)}, + big.NewInt(0), + }, errors.New("Key error")) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Contains(t, err.Error(), "Key error") +} + +func TestValidateKey_fail_mismatch_key(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + [32]byte{1}, + []*big.Int{big.NewInt(KeyPurposeSigning)}, + big.NewInt(0), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Contains(t, err.Error(), "Key doesn't match") +} + +func TestValidateKey_fail_missing_purpose(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + var key [32]byte + copy(key[:], pubKey) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + key, + nil, + big.NewInt(0), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Contains(t, err.Error(), "Key doesn't have purpose") +} + +func TestValidateKey_fail_wrong_purpose(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + var key [32]byte + copy(key[:], pubKey) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + key, + []*big.Int{big.NewInt(KeyPurposeP2P)}, + big.NewInt(0), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Contains(t, err.Error(), "Key doesn't have purpose") +} + +func TestValidateKey_fail_revocation(t *testing.T) { + centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + pubKey := utils.RandomSlice(32) + var key [32]byte + copy(key[:], pubKey) + c, f, r, g, i := &testingconfig.MockConfig{}, &MockIDFactory{}, &MockIDRegistry{}, &MockGethClient{}, &MockIDContract{} + g.On("GetGethCallOpts").Return(&bind.CallOpts{}, func() {}) + g.On("GetEthClient").Return(ðclient.Client{}) + r.On("GetIdentityByCentrifugeId", mock.Anything, centID.BigInt()).Return(common.BytesToAddress(utils.RandomSlice(20)), nil) + i.On("GetKey", mock.Anything, mock.Anything).Return(struct { + Key [32]byte + Purposes []*big.Int + RevokedAt *big.Int + }{ + key, + []*big.Int{big.NewInt(KeyPurposeSigning)}, + big.NewInt(1), + }, nil) + srv := NewEthereumIdentityService(c, f, r, nil, func() ethereum.Client { + return g + }, func(address common.Address, backend bind.ContractBackend) (contract, error) { + return i, nil + }) + err := srv.ValidateKey(centID, pubKey, KeyPurposeSigning) + f.AssertExpectations(t) + r.AssertExpectations(t) + g.AssertExpectations(t) + assert.Contains(t, err.Error(), "Key is currently revoked since block") +} diff --git a/identity/id_registration_confirmation_task.go b/identity/id_registration_confirmation_task.go index dcfe9be67..706c30e2d 100644 --- a/identity/id_registration_confirmation_task.go +++ b/identity/id_registration_confirmation_task.go @@ -2,11 +2,11 @@ package identity import ( "context" - "fmt" "math/big" "time" "github.com/centrifuge/go-centrifuge/centerrors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/gocelery" @@ -14,102 +14,109 @@ import ( ) const ( - IdRegistrationConfirmationTaskName string = "IdRegistrationConfirmationTaskName" + idRegistrationConfirmationTaskName string = "idRegistrationConfirmationTaskName" ) -type IdentitiesCreatedFilterer interface { - FilterIdentityCreated(opts *bind.FilterOpts, centrifugeId []*big.Int) (*EthereumIdentityFactoryContractIdentityCreatedIterator, error) +type identitiesCreatedFilterer interface { + FilterIdentityCreated(opts *bind.FilterOpts, centID []*big.Int) (*EthereumIdentityFactoryContractIdentityCreatedIterator, error) } -// IdRegistrationConfirmationTask is a queued task to watch ID registration events on Ethereum using EthereumIdentityFactoryContract. +// idRegistrationConfirmationTask is a queued task to watch ID registration events on Ethereum using EthereumIdentityFactoryContract. // To see how it gets registered see bootstrapper.go and to see how it gets used see setUpRegistrationEventListener method -type IdRegistrationConfirmationTask struct { - CentID CentID - BlockHeight uint64 - EthContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc) - EthContext context.Context - IdentityCreatedWatcher IdentitiesCreatedFilterer +type idRegistrationConfirmationTask struct { + centID CentID + blockHeight uint64 + timeout time.Duration + contextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc) + ctx context.Context + filterer identitiesCreatedFilterer } -func NewIdRegistrationConfirmationTask( - identityCreatedWatcher IdentitiesCreatedFilterer, - ethContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc), -) *IdRegistrationConfirmationTask { - return &IdRegistrationConfirmationTask{ - IdentityCreatedWatcher: identityCreatedWatcher, - EthContextInitializer: ethContextInitializer, +func newIDRegistrationConfirmationTask( + timeout time.Duration, + identityCreatedWatcher identitiesCreatedFilterer, + ethContextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc), +) *idRegistrationConfirmationTask { + return &idRegistrationConfirmationTask{ + timeout: timeout, + filterer: identityCreatedWatcher, + contextInitializer: ethContextInitializer, } } -func (rct *IdRegistrationConfirmationTask) Name() string { - return IdRegistrationConfirmationTaskName +// TaskTypeName returns the name of the task +func (rct *idRegistrationConfirmationTask) TaskTypeName() string { + return idRegistrationConfirmationTaskName } -func (rct *IdRegistrationConfirmationTask) Init() error { - queue.Queue.Register(IdRegistrationConfirmationTaskName, rct) - return nil -} - -func (rct *IdRegistrationConfirmationTask) Copy() (gocelery.CeleryTask, error) { - return &IdRegistrationConfirmationTask{ - rct.CentID, - rct.BlockHeight, - rct.EthContextInitializer, - rct.EthContext, - rct.IdentityCreatedWatcher}, nil +// Copy returns a new copy of the the task +func (rct *idRegistrationConfirmationTask) Copy() (gocelery.CeleryTask, error) { + return &idRegistrationConfirmationTask{ + rct.centID, + rct.blockHeight, + rct.timeout, + rct.contextInitializer, + rct.ctx, + rct.filterer}, nil } -// ParseKwargs - define a method to parse CentID -func (rct *IdRegistrationConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { - centId, ok := kwargs[CentIdParam] +// ParseKwargs parses the kwargs into the task. +func (rct *idRegistrationConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { + id, ok := kwargs[centIDParam] if !ok { - return fmt.Errorf("undefined kwarg " + CentIdParam) + return errors.New("undefined kwarg " + centIDParam) } - centIdTyped, err := getCentID(centId) + centID, err := getCentID(id) if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", CentIdParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", centIDParam, err.Error()) } - rct.CentID = centIdTyped + rct.centID = centID - rct.BlockHeight, err = parseBlockHeight(kwargs) + rct.blockHeight, err = queue.ParseBlockHeight(kwargs) if err != nil { return err } + + // override timeout param if provided + tdRaw, ok := kwargs[queue.TimeoutParam] + if ok { + td, err := queue.GetDuration(tdRaw) + if err != nil { + return errors.New("malformed kwarg [%s] because [%s]", queue.TimeoutParam, err.Error()) + } + rct.timeout = td + } + return nil } -// RunTask calls listens to events from geth related to IdRegistrationConfirmationTask#CentID and records result. -func (rct *IdRegistrationConfirmationTask) RunTask() (interface{}, error) { - log.Infof("Waiting for confirmation for the ID [%x]", rct.CentID.ByteArray()) - if rct.EthContext == nil { - rct.EthContext, _ = rct.EthContextInitializer() +// RunTask calls listens to events from geth related to idRegistrationConfirmationTask#CentID and records result. +func (rct *idRegistrationConfirmationTask) RunTask() (interface{}, error) { + log.Infof("Waiting for confirmation for the ID [%x]", rct.centID) + if rct.ctx == nil { + rct.ctx, _ = rct.contextInitializer(rct.timeout) } fOpts := &bind.FilterOpts{ - Context: rct.EthContext, - Start: rct.BlockHeight, + Context: rct.ctx, + Start: rct.blockHeight, } for { - iter, err := rct.IdentityCreatedWatcher.FilterIdentityCreated( - fOpts, - []*big.Int{rct.CentID.BigInt()}, - ) + iter, err := rct.filterer.FilterIdentityCreated(fOpts, []*big.Int{rct.centID.BigInt()}) if err != nil { return nil, centerrors.Wrap(err, "failed to start filtering identity event logs") } err = utils.LookForEvent(iter) if err == nil { - log.Infof("Received filtered event Id Registration Confirmation for CentrifugeId [%s]\n", rct.CentID.String()) + log.Infof("Received filtered event Id Registration Confirmation for CentrifugeID [%s]\n", rct.centID.String()) return iter.Event, nil } - if err != utils.EventNotFound { + if err != utils.ErrEventNotFound { return nil, err } time.Sleep(100 * time.Millisecond) } - - return nil, fmt.Errorf("failed to filter identity events") } diff --git a/identity/id_registration_confirmation_task_test.go b/identity/id_registration_confirmation_task_test.go index b590f59e8..960d9a09c 100644 --- a/identity/id_registration_confirmation_task_test.go +++ b/identity/id_registration_confirmation_task_test.go @@ -1,43 +1,45 @@ // +build unit -package identity_test +package identity import ( "testing" - "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" ) func TestRegistrationConfirmationTask_ParseKwargsHappyPath(t *testing.T) { - rct := identity.IdRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) + rct := idRegistrationConfirmationTask{} + id := utils.RandomSlice(CentIDLength) blockHeight := uint64(3132) - idBytes, _ := identity.ToCentID(id) + timeout := float64(3000) + idBytes, _ := ToCentID(id) kwargs := map[string]interface{}{ - identity.CentIdParam: idBytes, - identity.BlockHeight: blockHeight, + centIDParam: idBytes, + queue.BlockHeightParam: blockHeight, + queue.TimeoutParam: timeout, } - decoded, err := utils.SimulateJsonDecodeForGocelery(kwargs) + decoded, err := utils.SimulateJSONDecodeForGocelery(kwargs) err = rct.ParseKwargs(decoded) if err != nil { - t.Errorf("Could not parse %s for [%x]", identity.CentIdParam, id) + t.Errorf("Could not parse %s for [%x]", centIDParam, id) } - assert.Equal(t, idBytes, rct.CentID, "Resulting mockID should have the same ID as the input") - assert.Equal(t, blockHeight, rct.BlockHeight, "Resulting blockheight should be same as the input") + assert.Equal(t, idBytes, rct.centID, "Resulting mockID should have the same ID as the input") + assert.Equal(t, blockHeight, rct.blockHeight, "Resulting blockheight should be same as the input") } func TestRegistrationConfirmationTask_ParseKwargsDoesNotExist(t *testing.T) { - rct := identity.IdRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) + rct := idRegistrationConfirmationTask{} + id := utils.RandomSlice(CentIDLength) err := rct.ParseKwargs(map[string]interface{}{"notId": id}) assert.NotNil(t, err, "Should not allow parsing without centId") } func TestRegistrationConfirmationTask_ParseKwargsInvalidType(t *testing.T) { - rct := identity.IdRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) - err := rct.ParseKwargs(map[string]interface{}{identity.CentIdParam: id}) + rct := idRegistrationConfirmationTask{} + id := utils.RandomSlice(CentIDLength) + err := rct.ParseKwargs(map[string]interface{}{centIDParam: id}) assert.NotNil(t, err, "Should not parse without the correct type of centId") } diff --git a/identity/identity.go b/identity/identity.go index a5a076e6b..569747f1e 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -1,81 +1,54 @@ package identity import ( - "bytes" "context" - "errors" "fmt" "math/big" + "github.com/centrifuge/go-centrifuge/errors" + + "time" + + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/centerrors" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/centrifuge/go-centrifuge/keytools/secp256k1" + "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) const ( - CentIDLength = 6 - ActionCreate = "create" - ActionAddKey = "addkey" - KeyPurposeP2p = 1 - KeyPurposeSigning = 2 - KeyPurposeEthMsgAuth = 3 -) + // CentIDLength is the length in bytes of the CentrifugeID + CentIDLength = 6 -// IDService is a default implementation of the Service -var IDService Service + // KeyPurposeP2P represents a key used for p2p txns + KeyPurposeP2P = 1 -type CentID [CentIDLength]byte - -// ToCentID takes bytes and return CentID -// errors out if bytes are empty, nil, or len(bytes) > CentIDLength -func ToCentID(bytes []byte) (centID CentID, err error) { - if utils.IsEmptyByteSlice(bytes) { - return centID, fmt.Errorf("empty bytes provided") - } - - if !utils.IsValidByteSliceForLength(bytes, CentIDLength) { - return centID, errors.New("invalid length byte slice provided for centID") - } - - copy(centID[:], bytes[:CentIDLength]) - return centID, nil -} - -// CentIDFromString takes an hex string and returns a CentID -func CentIDFromString(id string) (centID CentID, err error) { - decID, err := hexutil.Decode(id) - if err != nil { - return centID, centerrors.Wrap(err, "failed to decode id") - } - - return ToCentID(decID) -} + // KeyPurposeSigning represents a key used for signing + KeyPurposeSigning = 2 -// CentIDsFromStrings converts hex ids to centIDs -func CentIDsFromStrings(ids []string) ([]CentID, error) { - var cids []CentID - for _, id := range ids { - cid, err := CentIDFromString(id) - if err != nil { - return nil, err - } + // KeyPurposeEthMsgAuth represents a key used for ethereum txns + KeyPurposeEthMsgAuth = 3 +) - cids = append(cids, cid) - } +// CentID represents a CentIDLength identity of an entity +type CentID [CentIDLength]byte - return cids, nil +// IDConfig holds information about the identity +type IDConfig struct { + ID CentID + Keys map[int]IDKey } -func NewRandomCentID() CentID { - ID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - return ID +// IDKey represents a key pair +type IDKey struct { + PublicKey []byte + PrivateKey []byte } +// Equal checks if c == other func (c CentID) Equal(other CentID) bool { for i := range c { if c[i] != other[i] { @@ -86,44 +59,33 @@ func (c CentID) Equal(other CentID) bool { return true } +// String returns the hex format of CentID func (c CentID) String() string { return hexutil.Encode(c[:]) } -func (c CentID) MarshalBinary() (data []byte, err error) { - return c[:], nil -} - +// BigInt returns CentID in bigInt func (c CentID) BigInt() *big.Int { return utils.ByteSliceToBigInt(c[:]) } -func (c CentID) ByteArray() [CentIDLength]byte { - var idBytes [CentIDLength]byte - copy(idBytes[:], c[:CentIDLength]) - return idBytes -} - -func ParseCentIDs(centIDByteArray [][]byte) (errs []error, centIDs []CentID) { - for _, element := range centIDByteArray { - centID, err := ToCentID(element) - if err != nil { - err = centerrors.Wrap(err, "error parsing receiver centId") - errs = append(errs, err) - continue - } - centIDs = append(centIDs, centID) - } - return errs, centIDs +// Config defines methods required for the package identity. +type Config interface { + GetEthereumDefaultAccountName() string + GetIdentityID() ([]byte, error) + GetSigningKeyPair() (pub, priv string) + GetEthAuthKeyPair() (pub, priv string) + GetEthereumContextWaitTimeout() time.Duration + GetContractAddress(address string) common.Address } // Identity defines an Identity on chain type Identity interface { fmt.Stringer - GetCentrifugeID() CentID - CentrifugeID(cenId CentID) - GetCurrentP2PKey() (ret string, err error) - GetLastKeyForPurpose(keyPurpose int) (key []byte, err error) + CentID() CentID + SetCentrifugeID(centID CentID) + CurrentP2PKey() (ret string, err error) + LastKeyForPurpose(keyPurpose int) (key []byte, err error) AddKeyToIdentity(ctx context.Context, keyPurpose int, key []byte) (confirmations chan *WatchIdentity, err error) FetchKey(key []byte) (Key, error) } @@ -135,12 +97,13 @@ type Key interface { GetRevokedAt() *big.Int } +// WatchIdentity holds the identity received form chain event type WatchIdentity struct { Identity Identity Error error } -// Service is used to fetch identities +// Service is used to interact with centrifuge identities type Service interface { // LookupIdentityForID looks up if the identity for given CentID exists on ethereum @@ -154,124 +117,123 @@ type Service interface { // GetIdentityAddress gets the address of the ethereum identity contract for the given CentID GetIdentityAddress(centID CentID) (common.Address, error) -} -// GetClientP2PURL returns the p2p url associated with the centID -func GetClientP2PURL(centID CentID) (url string, err error) { - target, err := IDService.LookupIdentityForID(centID) - if err != nil { - return url, centerrors.Wrap(err, "error fetching receiver identity") - } + // GetClientP2PURL returns the p2p url associated with the centID + GetClientP2PURL(centID CentID) (url string, err error) - p2pKey, err := target.GetCurrentP2PKey() - if err != nil { - return url, centerrors.Wrap(err, "error fetching p2p key") - } + // GetClientsP2PURLs returns p2p urls associated with each centIDs + // will error out at first failure + GetClientsP2PURLs(centIDs []CentID) ([]string, error) - return fmt.Sprintf("/ipfs/%s", p2pKey), nil -} + // GetIdentityKey returns the key for provided identity + GetIdentityKey(identity CentID, pubKey []byte) (keyInfo Key, err error) -// GetClientsP2PURLs returns p2p urls associated with each centIDs -// will error out at first failure -func GetClientsP2PURLs(centIDs []CentID) ([]string, error) { - var p2pURLs []string - for _, id := range centIDs { - url, err := GetClientP2PURL(id) - if err != nil { - return nil, err - } + // ValidateKey checks if a given key is valid for the given centrifugeID. + ValidateKey(centID CentID, key []byte, purpose int) error - p2pURLs = append(p2pURLs, url) - } + // AddKeyFromConfig adds a key previously generated and indexed in the configuration file to the identity specified in such config file + AddKeyFromConfig(purpose int) error - return p2pURLs, nil + // ValidateSignature validates a signature on a message based on identity data + ValidateSignature(signature *coredocumentpb.Signature, message []byte) error } -// GetIdentityKey returns the key for provided identity -func GetIdentityKey(identity CentID, pubKey []byte) (keyInfo Key, err error) { - id, err := IDService.LookupIdentityForID(identity) +// GetIdentityConfig returns the identity and keys associated with the node. +func GetIdentityConfig(config Config) (*IDConfig, error) { + centIDBytes, err := config.GetIdentityID() + if err != nil { + return nil, err + } + centID, err := ToCentID(centIDBytes) if err != nil { - return keyInfo, err + return nil, err } - key, err := id.FetchKey(pubKey) + //ed25519 keys + keys := map[int]IDKey{} + pk, sk, err := ed25519.GetSigningKeyPair(config.GetSigningKeyPair()) if err != nil { - return keyInfo, err + return nil, err } + keys[KeyPurposeP2P] = IDKey{PublicKey: pk, PrivateKey: sk} + keys[KeyPurposeSigning] = IDKey{PublicKey: pk, PrivateKey: sk} - if utils.IsEmptyByte32(key.GetKey()) { - return keyInfo, fmt.Errorf(fmt.Sprintf("key not found for identity: %x", identity)) + //secp256k1 keys + pk, sk, err = secp256k1.GetEthAuthKey(config.GetEthAuthKeyPair()) + if err != nil { + return nil, err + } + pubKey, err := hexutil.Decode(secp256k1.GetAddress(pk)) + if err != nil { + return nil, err } + keys[KeyPurposeEthMsgAuth] = IDKey{PublicKey: pubKey, PrivateKey: sk} - return key, nil + return &IDConfig{ID: centID, Keys: keys}, nil } -// ValidateKey checks if a given key is valid for the given centrifugeID. -func ValidateKey(centrifugeId CentID, key []byte, purpose int) error { - idKey, err := GetIdentityKey(centrifugeId, key) - if err != nil { - return err +// ToCentID takes bytes and return CentID +// errors out if bytes are empty, nil, or len(bytes) > CentIDLength +func ToCentID(bytes []byte) (centID CentID, err error) { + if utils.IsEmptyByteSlice(bytes) { + return centID, errors.New("empty bytes provided") } - if !bytes.Equal(key, utils.Byte32ToSlice(idKey.GetKey())) { - return fmt.Errorf(fmt.Sprintf("[Key: %x] Key doesn't match", idKey.GetKey())) + if !utils.IsValidByteSliceForLength(bytes, CentIDLength) { + return centID, errors.New("invalid length byte slice provided for centID") } - if !utils.ContainsBigIntInSlice(big.NewInt(int64(purpose)), idKey.GetPurposes()) { - return fmt.Errorf(fmt.Sprintf("[Key: %x] Key doesn't have purpose [%d]", idKey.GetKey(), purpose)) - } + copy(centID[:], bytes[:CentIDLength]) + return centID, nil +} - if idKey.GetRevokedAt().Cmp(big.NewInt(0)) != 0 { - return fmt.Errorf(fmt.Sprintf("[Key: %x] Key is currently revoked since block [%d]", idKey.GetKey(), idKey.GetRevokedAt())) +// CentIDFromString takes an hex string and returns a CentID +func CentIDFromString(id string) (centID CentID, err error) { + decID, err := hexutil.Decode(id) + if err != nil { + return centID, centerrors.Wrap(err, "failed to decode id") } - return nil + return ToCentID(decID) } -// AddKeyFromConfig adds a key previously generated and indexed in the configuration file to the identity specified in such config file -func AddKeyFromConfig(purpose int) error { - var identityConfig *config.IdentityConfig - var err error - - switch purpose { - case KeyPurposeP2p: - identityConfig, err = ed25519.GetIDConfig() - case KeyPurposeSigning: - identityConfig, err = ed25519.GetIDConfig() - case KeyPurposeEthMsgAuth: - identityConfig, err = secp256k1.GetIDConfig() - default: - err = errors.New("option not supported") - } +// CentIDsFromStrings converts hex ids to centIDs +func CentIDsFromStrings(ids []string) ([]CentID, error) { + var cids []CentID + for _, id := range ids { + cid, err := CentIDFromString(id) + if err != nil { + return nil, err + } - if err != nil { - return err + cids = append(cids, cid) } - centId, err := ToCentID(identityConfig.ID) - if err != nil { - return err - } + return cids, nil +} - id, err := IDService.LookupIdentityForID(centId) - if err != nil { - return err - } +// RandomCentID returns a randomly generated CentID +func RandomCentID() CentID { + ID, _ := ToCentID(utils.RandomSlice(CentIDLength)) + return ID +} - ctx, cancel := ethereum.DefaultWaitForTransactionMiningContext() - defer cancel() - confirmations, err := id.AddKeyToIdentity(ctx, purpose, identityConfig.PublicKey) +// ValidateCentrifugeIDBytes validates a centrifuge ID given as bytes +func ValidateCentrifugeIDBytes(givenCentID []byte, centrifugeID CentID) error { + centIDSignature, err := ToCentID(givenCentID) if err != nil { return err } - watchAddedToIdentity := <-confirmations - lastKey, errLocal := watchAddedToIdentity.Identity.GetLastKeyForPurpose(purpose) - if errLocal != nil { - return err + if !centrifugeID.Equal(centIDSignature) { + return errors.New("provided bytes doesn't match centID") } - log.Infof("Key [%v] with type [$s] Added to Identity [%s]", lastKey, purpose, watchAddedToIdentity.Identity) - return nil } + +// Sign the document with the private key and return the signature along with the public key for the verification +// assumes that signing root for the document is generated +func Sign(idConfig *IDConfig, purpose int, payload []byte) *coredocumentpb.Signature { + return signatures.Sign(idConfig.ID[:], idConfig.Keys[purpose].PrivateKey, idConfig.Keys[purpose].PublicKey, payload) +} diff --git a/identity/identity_test.go b/identity/identity_test.go index 984af649d..a0b0f0a7d 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -4,16 +4,37 @@ package identity import ( "context" - "fmt" - "math/big" + "os" "testing" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + +func TestMain(m *testing.M) { + ibootstappers := []bootstrap.TestBootstrapper{ + &config.Bootstrapper{}, + } + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + result := m.Run() + bootstrap.RunTestTeardown(ibootstappers) + os.Exit(result) +} + // mockID implements Identity type mockID struct { mock.Mock @@ -24,21 +45,21 @@ func (i *mockID) String() string { return args.String(0) } -func (i *mockID) GetCentrifugeID() CentID { +func (i *mockID) CentID() CentID { args := i.Called() return args.Get(0).(CentID) } -func (i *mockID) CentrifugeID(centId CentID) { +func (i *mockID) SetCentrifugeID(centId CentID) { i.Called(centId) } -func (i *mockID) GetCurrentP2PKey() (ret string, err error) { +func (i *mockID) CurrentP2PKey() (ret string, err error) { args := i.Called() return args.String(0), args.Error(1) } -func (i *mockID) GetLastKeyForPurpose(keyPurpose int) (key []byte, err error) { +func (i *mockID) LastKeyForPurpose(keyPurpose int) (key []byte, err error) { args := i.Called(keyPurpose) return args.Get(0).([]byte), args.Error(1) } @@ -48,11 +69,6 @@ func (i *mockID) AddKeyToIdentity(ctx context.Context, keyPurpose int, key []byt return args.Get(0).(chan *WatchIdentity), args.Error(1) } -func (i *mockID) CheckIdentityExists() (exists bool, err error) { - args := i.Called() - return args.Bool(0), args.Error(1) -} - func (i *mockID) FetchKey(key []byte) (Key, error) { args := i.Called(key) idKey := args.Get(0) @@ -96,6 +112,55 @@ func (srv *mockIDService) CheckIdentityExists(centID CentID) (exists bool, err e return args.Bool(0), args.Error(1) } +func TestGetIdentityConfig_Success(t *testing.T) { + idConfig, err := GetIdentityConfig(cfg) + assert.Nil(t, err) + assert.NotNil(t, idConfig) + configId, err := cfg.GetIdentityID() + assert.Nil(t, err) + idBytes := idConfig.ID[:] + assert.Equal(t, idBytes, configId) + assert.Equal(t, 3, len(idConfig.Keys)) +} + +func TestGetIdentityConfig_Error(t *testing.T) { + //Wrong Hex + currentId := cfg.GetString("identityId") + cfg.Set("identityId", "ABCD") + idConfig, err := GetIdentityConfig(cfg) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "hex string without 0x prefix") + assert.Nil(t, idConfig) + cfg.Set("identityId", currentId) + + //Wrong length + currentId = cfg.GetString("identityId") + cfg.Set("identityId", "0x0101010101") + idConfig, err = GetIdentityConfig(cfg) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid length byte slice provided for centID") + assert.Nil(t, idConfig) + cfg.Set("identityId", currentId) + + //Wrong public signing key path + currentKeyPath, _ := cfg.GetSigningKeyPair() + cfg.Set("keys.signing.publicKey", "./build/resources/signingKey.pub.pem") + idConfig, err = GetIdentityConfig(cfg) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + assert.Nil(t, idConfig) + cfg.Set("keys.signing.publicKey", currentKeyPath) + + //Wrong public ethauth key path + currentKeyPath, _ = cfg.GetEthAuthKeyPair() + cfg.Set("keys.ethauth.publicKey", "./build/resources/ethauth.pub.pem") + idConfig, err = GetIdentityConfig(cfg) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + assert.Nil(t, idConfig) + cfg.Set("keys.ethauth.publicKey", currentKeyPath) +} + func TestToCentId(t *testing.T) { tests := []struct { name string @@ -140,250 +205,6 @@ func TestNewCentIdEqual(t *testing.T) { assert.False(t, centrifugeIdA.Equal(centrifugeIdC), "centrifuge Id's should not be equal") } -func TestGetClientP2PURL_fail_service(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(nil, fmt.Errorf("failed service")).Once() - IDService = srv - p2p, err := GetClientP2PURL(centID) - srv.AssertExpectations(t) - assert.Empty(t, p2p, "p2p is not empty") - assert.Errorf(t, err, "error should not be nil") - assert.Contains(t, err.Error(), "failed service") -} - -func TestGetClientP2PURL_fail_identity(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - srv := &mockIDService{} - id := &mockID{} - id.On("GetCurrentP2PKey").Return("", fmt.Errorf("error identity")).Once() - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - IDService = srv - p2p, err := GetClientP2PURL(centID) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Empty(t, p2p, "p2p is not empty") - assert.Errorf(t, err, "error should not be nil") - assert.Contains(t, err.Error(), "error identity") -} - -func TestGetClientP2PURL_Success(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - srv := &mockIDService{} - id := &mockID{} - id.On("GetCurrentP2PKey").Return("target", nil).Once() - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - IDService = srv - p2p, err := GetClientP2PURL(centID) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Nil(t, err, "must be nil") - assert.Equal(t, p2p, "/ipfs/target") -} - -func TestGetClientsP2PURLs_fail(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - centIDs := []CentID{centID} - srv := &mockIDService{} - id := &mockID{} - id.On("GetCurrentP2PKey").Return("", fmt.Errorf("error identity")).Once() - srv.On("LookupIdentityForID", centIDs[0]).Return(id, nil).Once() - IDService = srv - p2pURLs, err := GetClientsP2PURLs(centIDs) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Empty(t, p2pURLs, "p2p is not empty") - assert.Errorf(t, err, "error should not be nil") - assert.Contains(t, err.Error(), "error identity") -} - -func TestGetClientsP2PURLs_success(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - centIDs := []CentID{centID} - id := &mockID{} - id.On("GetCurrentP2PKey").Return("target", nil).Once() - srv := &mockIDService{} - srv.On("LookupIdentityForID", centIDs[0]).Return(id, nil).Once() - IDService = srv - p2pURLs, err := GetClientsP2PURLs(centIDs) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Nil(t, err, "should be nil") - assert.NotEmpty(t, p2pURLs, "should not be empty") - assert.Equal(t, p2pURLs[0], "/ipfs/target") -} - -func TestGetIdentityKey_fail_lookup(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(nil, fmt.Errorf("lookup failed")).Once() - IDService = srv - id, err := GetIdentityKey(centID, utils.RandomSlice(32)) - srv.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "lookup failed") - assert.Nil(t, id, "must be nil") -} - -func TestGetIdentityKey_fail_FetchKey(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomSlice(32) - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(nil, fmt.Errorf("fetch key failed")).Once() - IDService = srv - key, err := GetIdentityKey(centID, pubKey) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "fetch key failed") - assert.Nil(t, key, "must be nil") -} - -func TestGetIdentityKey_fail_empty(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomSlice(32) - var emptyKey [32]byte - idkey := &EthereumIdentityKey{Key: emptyKey} - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - IDService = srv - key, err := GetIdentityKey(centID, pubKey) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "key not found for identity") - assert.Nil(t, key, "must be nil") -} - -func TestGetIdentityKey_Success(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomSlice(32) - pkey := utils.RandomByte32() - idkey := &EthereumIdentityKey{Key: pkey} - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - IDService = srv - key, err := GetIdentityKey(centID, pubKey) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Nil(t, err, "error must be nil") - assert.NotNil(t, key, "must not be nil") - assert.Equal(t, key, idkey) -} - -func TestValidateKey_fail_getId(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomSlice(32) - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(nil, fmt.Errorf("failed at GetIdentity")).Once() - IDService = srv - err := ValidateKey(centID, pubKey, KeyPurposeSigning) - srv.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "failed at GetIdentity") -} - -func TestValidateKey_fail_mismatch_key(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomSlice(32) - idkey := &EthereumIdentityKey{Key: utils.RandomByte32()} - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - IDService = srv - err := ValidateKey(centID, pubKey, KeyPurposeSigning) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), " Key doesn't match") -} - -func TestValidateKey_fail_missing_purpose(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomByte32() - idkey := &EthereumIdentityKey{Key: pubKey} - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey[:]).Return(idkey, nil).Once() - IDService = srv - err := ValidateKey(centID, pubKey[:], KeyPurposeSigning) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "Key doesn't have purpose") -} - -func TestValidateKey_fail_wrong_purpose(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomByte32() - idkey := &EthereumIdentityKey{ - Key: pubKey, - Purposes: []*big.Int{big.NewInt(KeyPurposeEthMsgAuth)}, - } - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey[:]).Return(idkey, nil).Once() - IDService = srv - err := ValidateKey(centID, pubKey[:], KeyPurposeSigning) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "Key doesn't have purpose") -} - -func TestValidateKey_fail_revocation(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomByte32() - idkey := &EthereumIdentityKey{ - Key: pubKey, - Purposes: []*big.Int{big.NewInt(KeyPurposeSigning)}, - RevokedAt: big.NewInt(1), - } - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey[:]).Return(idkey, nil).Once() - IDService = srv - err := ValidateKey(centID, pubKey[:], KeyPurposeSigning) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Error(t, err, "must be not nil") - assert.Contains(t, err.Error(), "Key is currently revoked since block") -} - -func TestValidateKey_success(t *testing.T) { - centID, _ := ToCentID(utils.RandomSlice(CentIDLength)) - pubKey := utils.RandomByte32() - idkey := &EthereumIdentityKey{ - Key: pubKey, - Purposes: []*big.Int{big.NewInt(KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &mockID{} - srv := &mockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey[:]).Return(idkey, nil).Once() - IDService = srv - err := ValidateKey(centID, pubKey[:], KeyPurposeSigning) - srv.AssertExpectations(t) - id.AssertExpectations(t) - assert.Nil(t, err, "must be nil") -} - -func TestAddKeyFromConfig_OptionNotSupported(t *testing.T) { - err := AddKeyFromConfig(4) - assert.NotNil(t, err, "it should error out") -} - func TestCentIDFromString(t *testing.T) { tests := []struct { id string @@ -397,17 +218,17 @@ func TestCentIDFromString(t *testing.T) { { id: "0x01020304050607", - err: fmt.Errorf("invalid length byte slice provided for centID"), + err: errors.New("invalid length byte slice provided for centID"), }, { id: "0xsome random", - err: fmt.Errorf("failed to decode id"), + err: errors.New("failed to decode id"), }, { id: "some random", - err: fmt.Errorf("hex string without 0x"), + err: errors.New("hex string without 0x"), }, } @@ -437,3 +258,29 @@ func TestCentIDsFromStrings(t *testing.T) { assert.NotNil(t, cids) assert.Equal(t, cids, []CentID{{1, 2, 3, 4, 5, 6}, {2, 3, 1, 2, 3, 4}}) } + +func TestValidateCentrifugeIDBytes(t *testing.T) { + c := RandomCentID() + assert.True(t, ValidateCentrifugeIDBytes(c[:], c) == nil) + + err := ValidateCentrifugeIDBytes(utils.RandomSlice(20), c) + if assert.Error(t, err) { + assert.Equal(t, "invalid length byte slice provided for centID", err.Error()) + } + + err = ValidateCentrifugeIDBytes(utils.RandomSlice(6), c) + if assert.Error(t, err) { + assert.Equal(t, "provided bytes doesn't match centID", err.Error()) + } +} + +func TestSign(t *testing.T) { + 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} + c := RandomCentID() + msg := utils.RandomSlice(100) + sig := Sign(&IDConfig{c, map[int]IDKey{KeyPurposeSigning: {PrivateKey: key1, PublicKey: key1Pub}}}, KeyPurposeSigning, msg) + + err := signatures.VerifySignature(key1Pub, msg, sig.Signature) + assert.True(t, err == nil) +} diff --git a/identity/key_registration_confirmation_task.go b/identity/key_registration_confirmation_task.go index ad5469b82..5cac6f7c1 100644 --- a/identity/key_registration_confirmation_task.go +++ b/identity/key_registration_confirmation_task.go @@ -2,158 +2,177 @@ package identity import ( "context" - "fmt" "math/big" "time" "github.com/centrifuge/go-centrifuge/centerrors" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/gocelery" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" ) const ( - KeyRegistrationConfirmationTaskName string = "KeyRegistrationConfirmationTaskName" - KeyParam string = "KeyParam" - KeyPurposeParam = "KeyPurposeParam" + keyRegistrationConfirmationTaskName = "keyRegistrationConfirmationTaskName" + keyParam = "keyParam" + keyPurposeParam = "keyPurposeParam" ) -type KeyRegisteredFilterer interface { +type keyRegisteredFilterer interface { FilterKeyAdded(opts *bind.FilterOpts, key [][32]byte, purpose []*big.Int) (*EthereumIdentityContractKeyAddedIterator, error) } -// KeyRegistrationConfirmationTask is a queued task to filter key registration events on Ethereum using EthereumIdentityContract. +// keyRegistrationConfirmationTask is a queued task to filter key registration events on Ethereum using EthereumIdentityContract. // To see how it gets registered see bootstrapper.go and to see how it gets used see setUpKeyRegisteredEventListener method -type KeyRegistrationConfirmationTask struct { - CentID CentID - Key [32]byte - KeyPurpose int - BlockHeight uint64 - EthContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc) - EthContext context.Context - KeyRegisteredWatcher KeyRegisteredFilterer - RegistryContract *EthereumIdentityRegistryContract - Config Config +type keyRegistrationConfirmationTask struct { + centID CentID + key [32]byte + keyPurpose int + blockHeight uint64 + timeout time.Duration + contextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc) + ctx context.Context + filterer keyRegisteredFilterer + contract *EthereumIdentityRegistryContract + config Config + gethClientFinder func() ethereum.Client + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error) + queue *queue.Server } -func NewKeyRegistrationConfirmationTask( - ethContextInitializer func() (ctx context.Context, cancelFunc context.CancelFunc), +func newKeyRegistrationConfirmationTask( + ethContextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc), registryContract *EthereumIdentityRegistryContract, config Config, -) *KeyRegistrationConfirmationTask { - return &KeyRegistrationConfirmationTask{ - EthContextInitializer: ethContextInitializer, - RegistryContract: registryContract, - Config: config, + queue *queue.Server, + gethClientFinder func() ethereum.Client, + contractProvider func(address common.Address, backend bind.ContractBackend) (contract, error), +) *keyRegistrationConfirmationTask { + return &keyRegistrationConfirmationTask{ + contextInitializer: ethContextInitializer, + contract: registryContract, + config: config, + queue: queue, + timeout: config.GetEthereumContextWaitTimeout(), + gethClientFinder: gethClientFinder, + contractProvider: contractProvider, } } -func (krct *KeyRegistrationConfirmationTask) Name() string { - return KeyRegistrationConfirmationTaskName +// TaskTypeName returns keyRegistrationConfirmationTaskName +func (krct *keyRegistrationConfirmationTask) TaskTypeName() string { + return keyRegistrationConfirmationTaskName } -func (krct *KeyRegistrationConfirmationTask) Init() error { - queue.Queue.Register(KeyRegistrationConfirmationTaskName, krct) - return nil -} - -func (krct *KeyRegistrationConfirmationTask) Copy() (gocelery.CeleryTask, error) { - return &KeyRegistrationConfirmationTask{ - krct.CentID, - krct.Key, - krct.KeyPurpose, - krct.BlockHeight, - krct.EthContextInitializer, - krct.EthContext, - krct.KeyRegisteredWatcher, - krct.RegistryContract, - krct.Config}, nil +// Copy returns a new copy of the task +func (krct *keyRegistrationConfirmationTask) Copy() (gocelery.CeleryTask, error) { + return &keyRegistrationConfirmationTask{ + krct.centID, + krct.key, + krct.keyPurpose, + krct.blockHeight, + krct.timeout, + krct.contextInitializer, + krct.ctx, + krct.filterer, + krct.contract, + krct.config, + krct.gethClientFinder, + krct.contractProvider, + krct.queue, + }, nil } -// ParseKwargs - define a method to parse params -func (krct *KeyRegistrationConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { - centId, ok := kwargs[CentIdParam] +// ParseKwargs parses the args into the task +func (krct *keyRegistrationConfirmationTask) ParseKwargs(kwargs map[string]interface{}) error { + id, ok := kwargs[centIDParam] if !ok { - return fmt.Errorf("undefined kwarg " + CentIdParam) + return errors.New("undefined kwarg " + centIDParam) } - centIdTyped, err := getCentID(centId) + centID, err := getCentID(id) if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", CentIdParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", centIDParam, err.Error()) } - krct.CentID = centIdTyped + krct.centID = centID // key parsing - key, ok := kwargs[KeyParam] + key, ok := kwargs[keyParam] if !ok { - return fmt.Errorf("undefined kwarg " + KeyParam) + return errors.New("undefined kwarg " + keyParam) } keyTyped, err := getBytes32(key) if err != nil { - return fmt.Errorf("malformed kwarg [%s] because [%s]", KeyParam, err.Error()) + return errors.New("malformed kwarg [%s] because [%s]", keyParam, err.Error()) } - krct.Key = keyTyped + krct.key = keyTyped // key purpose parsing - keyPurpose, ok := kwargs[KeyPurposeParam] + keyPurpose, ok := kwargs[keyPurposeParam] if !ok { - return fmt.Errorf("undefined kwarg " + KeyPurposeParam) + return errors.New("undefined kwarg " + keyPurposeParam) } keyPurposeF, ok := keyPurpose.(float64) if ok { - krct.KeyPurpose = int(keyPurposeF) + krct.keyPurpose = int(keyPurposeF) } else { - return fmt.Errorf("can not parse " + KeyPurposeParam) + return errors.New("can not parse " + keyPurposeParam) } // block height parsing - krct.BlockHeight, err = parseBlockHeight(kwargs) + krct.blockHeight, err = queue.ParseBlockHeight(kwargs) if err != nil { return err } + + tdRaw, ok := kwargs[queue.TimeoutParam] + if ok { + td, err := queue.GetDuration(tdRaw) + if err != nil { + return errors.New("malformed kwarg [%s] because [%s]", queue.TimeoutParam, err.Error()) + } + krct.timeout = td + } + return nil } -// RunTask calls listens to events from geth related to KeyRegistrationConfirmationTask#Key and records result. -func (krct *KeyRegistrationConfirmationTask) RunTask() (interface{}, error) { - log.Infof("Waiting for confirmation for the Key [%x]", krct.Key) - if krct.EthContext == nil { - krct.EthContext, _ = krct.EthContextInitializer() +// RunTask calls listens to events from geth related to keyRegistrationConfirmationTask#Key and records result. +func (krct *keyRegistrationConfirmationTask) RunTask() (interface{}, error) { + log.Infof("Waiting for confirmation for the Key [%x]", krct.key) + if krct.ctx == nil { + krct.ctx, _ = krct.contextInitializer(krct.timeout) } - id := EthereumIdentity{CentrifugeId: krct.CentID, RegistryContract: krct.RegistryContract, Config: krct.Config} + id := newEthereumIdentity(krct.centID, krct.contract, krct.config, krct.queue, krct.gethClientFinder, krct.contractProvider) contract, err := id.getContract() if err != nil { return nil, err } - krct.KeyRegisteredWatcher = contract + krct.filterer = contract fOpts := &bind.FilterOpts{ - Context: krct.EthContext, - Start: krct.BlockHeight, + Context: krct.ctx, + Start: krct.blockHeight, } for { - iter, err := krct.KeyRegisteredWatcher.FilterKeyAdded( - fOpts, - [][32]byte{krct.Key}, - []*big.Int{big.NewInt(int64(krct.KeyPurpose))}, - ) + iter, err := krct.filterer.FilterKeyAdded(fOpts, [][32]byte{krct.key}, []*big.Int{big.NewInt(int64(krct.keyPurpose))}) if err != nil { return nil, centerrors.Wrap(err, "failed to start filtering key event logs") } err = utils.LookForEvent(iter) if err == nil { - log.Infof("Received filtered event Key Registration Confirmation for CentrifugeId [%s] and key [%x] with purpose [%d]\n", krct.CentID.String(), krct.Key, krct.KeyPurpose) + log.Infof("Received filtered event Key Registration Confirmation for CentrifugeID [%s] and key [%x] with purpose [%d]\n", krct.centID.String(), krct.key, krct.keyPurpose) return iter.Event, nil } - if err != utils.EventNotFound { + if err != utils.ErrEventNotFound { return nil, err } time.Sleep(100 * time.Millisecond) } - - return nil, fmt.Errorf("failed to filter key events") } diff --git a/identity/key_registration_confirmation_task_test.go b/identity/key_registration_confirmation_task_test.go index f433e444f..4e6115948 100644 --- a/identity/key_registration_confirmation_task_test.go +++ b/identity/key_registration_confirmation_task_test.go @@ -1,51 +1,82 @@ // +build unit -package identity_test +package identity import ( "testing" - "github.com/centrifuge/go-centrifuge/identity" + "time" + + "github.com/centrifuge/go-centrifuge/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" ) func TestKeyRegistrationConfirmationTask_ParseKwargsHappyPath(t *testing.T) { - rct := identity.KeyRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) + rct := keyRegistrationConfirmationTask{timeout: time.Second * 10} + id := utils.RandomSlice(CentIDLength) + key := utils.RandomSlice(32) + var keyFixed [32]byte + copy(keyFixed[:], key) + keyPurpose := KeyPurposeSigning + bh := uint64(12) + idBytes, _ := ToCentID(id) + kwargs := map[string]interface{}{ + centIDParam: idBytes, + keyParam: keyFixed, + keyPurposeParam: keyPurpose, + queue.BlockHeightParam: bh, + } + decoded, err := utils.SimulateJSONDecodeForGocelery(kwargs) + err = rct.ParseKwargs(decoded) + if err != nil { + t.Errorf("parse error %s", err.Error()) + } + assert.Equal(t, idBytes, rct.centID, "Resulting mockID should have the same ID as the input") + assert.Equal(t, keyFixed, rct.key, "Resulting key should be same as the input") + assert.Equal(t, keyPurpose, rct.keyPurpose, "Resulting keyPurpose should be same as the input") + assert.Equal(t, bh, rct.blockHeight, "Resulting blockheight should be same as the input") +} + +func TestKeyRegistrationConfirmationTask_ParseKwargsHappyPathOverrideTimeout(t *testing.T) { + rct := keyRegistrationConfirmationTask{timeout: time.Second * 10} + id := utils.RandomSlice(CentIDLength) key := utils.RandomSlice(32) var keyFixed [32]byte copy(keyFixed[:], key) - keyPurpose := identity.KeyPurposeSigning - blockHeight := uint64(12) - idBytes, _ := identity.ToCentID(id) + keyPurpose := KeyPurposeSigning + bh := uint64(12) + idBytes, _ := ToCentID(id) + overrideTimeout := float64(time.Second * 3) kwargs := map[string]interface{}{ - identity.CentIdParam: idBytes, - identity.KeyParam: keyFixed, - identity.KeyPurposeParam: keyPurpose, - identity.BlockHeight: blockHeight, + centIDParam: idBytes, + keyParam: keyFixed, + keyPurposeParam: keyPurpose, + queue.BlockHeightParam: bh, + queue.TimeoutParam: overrideTimeout, } - decoded, err := utils.SimulateJsonDecodeForGocelery(kwargs) + decoded, err := utils.SimulateJSONDecodeForGocelery(kwargs) err = rct.ParseKwargs(decoded) if err != nil { t.Errorf("parse error %s", err.Error()) } - assert.Equal(t, idBytes, rct.CentID, "Resulting mockID should have the same ID as the input") - assert.Equal(t, keyFixed, rct.Key, "Resulting key should be same as the input") - assert.Equal(t, keyPurpose, rct.KeyPurpose, "Resulting keyPurpose should be same as the input") - assert.Equal(t, blockHeight, rct.BlockHeight, "Resulting blockheight should be same as the input") + assert.Equal(t, idBytes, rct.centID, "Resulting mockID should have the same ID as the input") + assert.Equal(t, keyFixed, rct.key, "Resulting key should be same as the input") + assert.Equal(t, keyPurpose, rct.keyPurpose, "Resulting keyPurpose should be same as the input") + assert.Equal(t, bh, rct.blockHeight, "Resulting blockheight should be same as the input") + assert.Equal(t, time.Duration(overrideTimeout).Seconds(), rct.timeout.Seconds(), "Resulting timeout should be overwritten by kwargs") } func TestKeyRegistrationConfirmationTask_ParseKwargsDoesNotExist(t *testing.T) { - rct := identity.KeyRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) + rct := keyRegistrationConfirmationTask{} + id := utils.RandomSlice(CentIDLength) err := rct.ParseKwargs(map[string]interface{}{"notId": id}) assert.NotNil(t, err, "Should not allow parsing without centId") } func TestKeyRegistrationConfirmationTask_ParseKwargsInvalidType(t *testing.T) { - rct := identity.KeyRegistrationConfirmationTask{} - id := utils.RandomSlice(identity.CentIDLength) - err := rct.ParseKwargs(map[string]interface{}{identity.CentIdParam: id}) + rct := keyRegistrationConfirmationTask{} + id := utils.RandomSlice(CentIDLength) + err := rct.ParseKwargs(map[string]interface{}{centIDParam: id}) assert.NotNil(t, err, "Should not parse without the correct type of centId") } diff --git a/identity/util.go b/identity/util.go index 2a0fb2f33..25fe92ff9 100644 --- a/identity/util.go +++ b/identity/util.go @@ -1,17 +1,16 @@ package identity -import "errors" +import "github.com/centrifuge/go-centrifuge/errors" const ( - CentIdParam string = "CentID" - BlockHeight string = "BlockHeight" + centIDParam string = "CentID" ) func getBytes32(key interface{}) ([32]byte, error) { var fixed [32]byte b, ok := key.([]interface{}) if !ok { - return fixed, errors.New("Could not parse interface to []byte") + return fixed, errors.New("could not parse interface to []byte") } // convert and copy b byte values for i, v := range b { @@ -25,7 +24,7 @@ func getCentID(key interface{}) (CentID, error) { var fixed [CentIDLength]byte b, ok := key.([]interface{}) if !ok { - return fixed, errors.New("Could not parse interface to []byte") + return fixed, errors.New("could not parse interface to []byte") } // convert and copy b byte values for i, v := range b { @@ -34,15 +33,3 @@ func getCentID(key interface{}) (CentID, error) { } return fixed, nil } - -func parseBlockHeight(valMap map[string]interface{}) (uint64, error) { - if bhi, ok := valMap[BlockHeight]; ok { - bhf, ok := bhi.(float64) - if ok { - return uint64(bhf), nil - } else { - return 0, errors.New("value can not be parsed") - } - } - return 0, errors.New("value can not be parsed") -} diff --git a/keytools/ed25519/ed25519.go b/keytools/ed25519/ed25519.go index 40a76a737..0f8dbe003 100644 --- a/keytools/ed25519/ed25519.go +++ b/keytools/ed25519/ed25519.go @@ -1,9 +1,7 @@ package ed25519 import ( - "fmt" - - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p-crypto" @@ -17,7 +15,7 @@ var log = logging.Logger("ed25519") func GetPublicSigningKey(fileName string) (publicKey ed25519.PublicKey, err error) { key, err := utils.ReadKeyFromPemFile(fileName, utils.PublicKey) if err != nil { - return nil, fmt.Errorf("failed to read pem file: %v", err) + return nil, errors.New("failed to read pem file: %v", err) } return ed25519.PublicKey(key), nil @@ -27,23 +25,22 @@ func GetPublicSigningKey(fileName string) (publicKey ed25519.PublicKey, err erro func GetPrivateSigningKey(fileName string) (privateKey ed25519.PrivateKey, err error) { key, err := utils.ReadKeyFromPemFile(fileName, utils.PrivateKey) if err != nil { - return nil, fmt.Errorf("failed to read pem file: %v", err) + return nil, errors.New("failed to read pem file: %v", err) } return ed25519.PrivateKey(key), nil } -// GetSigningKeyPairFromConfig returns the public and private key pair from the config -func GetSigningKeyPairFromConfig() (publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, err error) { - pub, priv := config.Config.GetSigningKeyPair() +// GetSigningKeyPair returns the public and private key pair +func GetSigningKeyPair(pub, priv string) (publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, err error) { publicKey, err = GetPublicSigningKey(pub) if err != nil { - return nil, nil, fmt.Errorf("failed to read public key: %v", err) + return nil, nil, errors.New("failed to read public key: %v", err) } privateKey, err = GetPrivateSigningKey(priv) if err != nil { - return nil, nil, fmt.Errorf("failed to read private key: %v", err) + return nil, nil, errors.New("failed to read private key: %v", err) } return publicKey, privateKey, nil @@ -55,39 +52,15 @@ func GenerateSigningKeyPair() (publicKey ed25519.PublicKey, privateKey ed25519.P if err != nil { log.Fatal(err) } - return + return publicKey, privateKey } // PublicKeyToP2PKey returns p2pId from the public key -func PublicKeyToP2PKey(publicKey [32]byte) (p2pId peer.ID, err error) { +func PublicKeyToP2PKey(publicKey [32]byte) (p2pID peer.ID, err error) { pk, err := crypto.UnmarshalEd25519PublicKey(publicKey[:]) if err != nil { - return "", err - } - - p2pId, err = peer.IDFromPublicKey(pk) - if err != nil { - return "", err - } - return -} - -// GetIDConfig reads the keys and ID from the config and returns a the Identity config -func GetIDConfig() (identityConfig *config.IdentityConfig, err error) { - pk, pvk, err := GetSigningKeyPairFromConfig() - if err != nil { - return nil, fmt.Errorf("failed to get signing keys: %v", err) - } - - centID, err := config.Config.GetIdentityID() - if err != nil { - return nil, err + return p2pID, err } - identityConfig = &config.IdentityConfig{ - ID: centID, - PublicKey: pk, - PrivateKey: pvk, - } - return + return peer.IDFromPublicKey(pk) } diff --git a/keytools/ed25519/ed25519_test.go b/keytools/ed25519/ed25519_test.go index c3c4d7b3e..7f333cc55 100644 --- a/keytools/ed25519/ed25519_test.go +++ b/keytools/ed25519/ed25519_test.go @@ -11,11 +11,15 @@ import ( "github.com/stretchr/testify/assert" ) +var ctx = map[string]interface{}{} +var cfg config.Configuration + func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &config.Bootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) result := m.Run() os.Exit(result) } @@ -34,59 +38,30 @@ func TestPublicKeyToP2PKey(t *testing.T) { } func TestGetSigningKeyPairFromConfig(t *testing.T) { - pub := config.Config.V.Get("keys.signing.publicKey") - pri := config.Config.V.Get("keys.signing.privateKey") + pub := cfg.Get("keys.signing.publicKey") + pri := cfg.Get("keys.signing.privateKey") // bad public key path - config.Config.V.Set("keys.signing.publicKey", "bad path") - pubK, priK, err := GetSigningKeyPairFromConfig() + cfg.Set("keys.signing.publicKey", "bad path") + pubK, priK, err := GetSigningKeyPair(cfg.GetSigningKeyPair()) assert.Error(t, err) assert.Nil(t, priK) assert.Nil(t, pubK) assert.Contains(t, err.Error(), "failed to read public key") - config.Config.V.Set("keys.signing.publicKey", pub) + cfg.Set("keys.signing.publicKey", pub) // bad private key path - config.Config.V.Set("keys.signing.privateKey", "bad path") - pubK, priK, err = GetSigningKeyPairFromConfig() + cfg.Set("keys.signing.privateKey", "bad path") + pubK, priK, err = GetSigningKeyPair(cfg.GetSigningKeyPair()) assert.Error(t, err) assert.Nil(t, priK) assert.Nil(t, pubK) assert.Contains(t, err.Error(), "failed to read private key") - config.Config.V.Set("keys.signing.privateKey", pri) + cfg.Set("keys.signing.privateKey", pri) // success - pubK, priK, err = GetSigningKeyPairFromConfig() + pubK, priK, err = GetSigningKeyPair(cfg.GetSigningKeyPair()) assert.Nil(t, err) assert.NotNil(t, pubK) assert.NotNil(t, priK) } - -func TestGetIDConfig(t *testing.T) { - pub := config.Config.V.Get("keys.signing.publicKey") - - // failed keys - config.Config.V.Set("keys.signing.publicKey", "bad path") - id, err := GetIDConfig() - assert.Error(t, err) - assert.Nil(t, id) - assert.Contains(t, err.Error(), "failed to get signing keys") - config.Config.V.Set("keys.signing.publicKey", pub) - - // failed identity - gID := config.Config.V.Get("identityId") - config.Config.V.Set("identityId", "bad id") - id, err = GetIDConfig() - assert.Error(t, err) - assert.Nil(t, id) - assert.Contains(t, err.Error(), "can't read identityId from config") - config.Config.V.Set("identityId", gID) - - // success - id, err = GetIDConfig() - assert.Nil(t, err) - assert.NotNil(t, id) - nID, err := config.Config.GetIdentityID() - assert.Nil(t, err) - assert.Equal(t, id.ID, nID) -} diff --git a/keytools/generate.go b/keytools/generate.go index 9916871dc..5806d67f8 100644 --- a/keytools/generate.go +++ b/keytools/generate.go @@ -8,6 +8,7 @@ import ( "github.com/centrifuge/go-centrifuge/utils" ) +// GenerateSigningKeyPair generates based on the curveType and writes keys to file paths given. func GenerateSigningKeyPair(publicFileName, privateFileName, curveType string) { var publicKey, privateKey []byte switch strings.ToLower(curveType) { diff --git a/keytools/generate_test.go b/keytools/generate_test.go index f720e9eca..0dd9c1a7c 100644 --- a/keytools/generate_test.go +++ b/keytools/generate_test.go @@ -18,54 +18,41 @@ const ( ) func GenerateKeyFilesForTest(t *testing.T, curve string) (publicKey, privateKey []byte) { - publicFileName := "publicKeyFile" privateFileName := "privateKeyFile" - GenerateSigningKeyPair(publicFileName, privateFileName, curve) _, err := os.Stat(publicFileName) - assert.False(t, err != nil, "public key file not generated") _, err = os.Stat(privateFileName) - assert.False(t, err != nil, "private key file not generated") publicKey, err = utils.ReadKeyFromPemFile(publicFileName, utils.PublicKey) - if err != nil { log.Fatal(err) } privateKey, err = utils.ReadKeyFromPemFile(privateFileName, utils.PrivateKey) - if err != nil { log.Fatal(err) } os.Remove(publicFileName) os.Remove(privateFileName) - - return - + return publicKey, privateKey } func TestGenerateSigningKeyPairSECP256K1(t *testing.T) { - curve := CurveSecp256K1 publicKey, privateKey := GenerateKeyFilesForTest(t, curve) - assert.Equal(t, len(publicKey), PublicKeySECP256K1Len, "public key length not correct") assert.Equal(t, len(privateKey), PrivateKeySECP256K1Len, "private key length not correct") - } func TestGenerateSigningKeyPairED25519(t *testing.T) { - curve := CurveEd25519 publicKey, privateKey := GenerateKeyFilesForTest(t, curve) - assert.Equal(t, len(publicKey), PublicKeyED25519Len, "public key length not correct") assert.Equal(t, len(privateKey), PrivateKeyED25519Len, "private key length not correct") } diff --git a/keytools/keytools.go b/keytools/keytools.go index 7a44379c0..c17c79bde 100644 --- a/keytools/keytools.go +++ b/keytools/keytools.go @@ -6,9 +6,9 @@ import ( var log = logging.Logger("keytools") +// Constants shared within subfolders const ( CurveEd25519 string = "ed25519" CurveSecp256K1 string = "secp256k1" + MaxMsgLen = 32 ) - -const MaxMsgLen = 32 diff --git a/keytools/secp256k1/secp256k1.go b/keytools/secp256k1/secp256k1.go index b50db3678..fdb540a2e 100644 --- a/keytools/secp256k1/secp256k1.go +++ b/keytools/secp256k1/secp256k1.go @@ -6,7 +6,7 @@ import ( "crypto/rand" "fmt" - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -17,13 +17,15 @@ import ( var log = logging.Logger("signing") -const SignatureRSFormatLen = 64 //64 byte [R || S] format -const SignatureRSVFormatLen = 65 //65 byte [R || S || V] format -const SignatureVPosition = 64 -const PrivateKeyLen = 32 +const ( + signatureRSFormatLen = 64 //64 byte [R || S] format + signatureRSVFormatLen = 65 //65 byte [R || S || V] format + signatureVPosition = 64 + privateKeyLen = 32 +) +// GenerateSigningKeyPair generates secp2562k1 based keys. func GenerateSigningKeyPair() (publicKey, privateKey []byte) { - log.Debug("generate secp256k1 keys") key, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader) if err != nil { @@ -31,18 +33,19 @@ func GenerateSigningKeyPair() (publicKey, privateKey []byte) { } publicKey = elliptic.Marshal(secp256k1.S256(), key.X, key.Y) - privateKey = make([]byte, PrivateKeyLen) + privateKey = make([]byte, privateKeyLen) blob := key.D.Bytes() - copy(privateKey[PrivateKeyLen-len(blob):], blob) + copy(privateKey[privateKeyLen-len(blob):], blob) return publicKey, privateKey } +// Sign signs the message using private key func Sign(message []byte, privateKey []byte) (signature []byte, err error) { return secp256k1.Sign(message, privateKey) - } +// SignEthereum converts message to ethereum specific format and signs it. func SignEthereum(message []byte, privateKey []byte) (signature []byte, err error) { // The hash is calculated in Ethereum in the following way // keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). @@ -50,108 +53,80 @@ func SignEthereum(message []byte, privateKey []byte) (signature []byte, err erro return Sign(hash, privateKey) } +// GetAddress returns the hex of first 20 bytes of the Keccak256 has of public keuy func GetAddress(publicKey []byte) string { - hash := crypto.Keccak256(publicKey[1:]) address := hash[12:] //address is the last 20 bytes of the hash len(hash) = 20 return hexutil.Encode(address) } +// VerifySignatureWithAddress verifies the signature using address provided func VerifySignatureWithAddress(address, sigHex string, msg []byte) bool { fromAddr := common.HexToAddress(address) - sig, err := hexutil.Decode(sigHex) - if err != nil { log.Error(err.Error()) return false } - if len(sig) != SignatureRSVFormatLen { + if len(sig) != signatureRSVFormatLen { log.Error("signature must be 65 bytes long") return false } // see implementation in go-ethereum for further details // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 - if sig[SignatureVPosition] != 0 && sig[SignatureVPosition] != 1 { - if sig[SignatureVPosition] != 27 && sig[SignatureVPosition] != 28 { + if sig[signatureVPosition] != 0 && sig[signatureVPosition] != 1 { + if sig[signatureVPosition] != 27 && sig[signatureVPosition] != 28 { log.Error("V value in signature has to be 27 or 28") return false } - sig[SignatureVPosition] -= 27 // change V value to 0 or 1 + sig[signatureVPosition] -= 27 // change V value to 0 or 1 } pubKey, err := crypto.SigToPub(SignHash(msg), sig) if err != nil { - return false } recoveredAddr := crypto.PubkeyToAddress(*pubKey) - return fromAddr == recoveredAddr } +// SignHash returns the hash of the data. // The hash is calculated as // keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). // for further details see // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L404 - func SignHash(data []byte) []byte { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(data)) return crypto.Keccak256(append([]byte(msg), data...)) } +// VerifySignature verifies signature using the public key provided. func VerifySignature(publicKey, message, signature []byte) bool { - if len(signature) == SignatureRSFormatLen+1 { + if len(signature) == signatureRSFormatLen+1 { // signature in [R || S || V] format is 65 bytes //https://bitcoin.stackexchange.com/questions/38351/ecdsa-v-r-s-what-is-v - - signature = signature[0:SignatureRSFormatLen] + signature = signature[0:signatureRSFormatLen] } // the signature should have the 64 byte [R || S] format return secp256k1.VerifySignature(publicKey, message, signature) } -// GetIDConfig reads the keys and ID from the config and returns a the Identity config -func GetIDConfig() (identityConfig *config.IdentityConfig, err error) { - pub, pvk, err := GetEthAuthKeyFromConfig() - if err != nil { - return nil, fmt.Errorf("failed to get eth keys: %v", err) - } - - centId, err := config.Config.GetIdentityID() - if err != nil { - return nil, err - } - - pubKey, err := hexutil.Decode(GetAddress(pub)) - if err != nil { - return nil, err - } - identityConfig = &config.IdentityConfig{ - ID: centId, - PublicKey: pubKey, - PrivateKey: pvk, - } - - return -} - -// GetEthAuthKeyFromConfig returns the public and private keys as byte array -func GetEthAuthKeyFromConfig() (public, private []byte, err error) { - pub, priv := config.Config.GetEthAuthKeyPair() +// GetEthAuthKey returns the public and private keys as byte array +func GetEthAuthKey(pub, priv string) (public, private []byte, err error) { privateKey, err := GetPrivateEthAuthKey(priv) if err != nil { - return nil, nil, fmt.Errorf("failed to read private key: %v", err) + return nil, nil, errors.New("failed to read private key: %v", err) } publicKey, err := GetPublicEthAuthKey(pub) if err != nil { - return nil, nil, fmt.Errorf("failed to read public key: %v", err) + return nil, nil, errors.New("failed to read public key: %v", err) } + return publicKey, privateKey, nil } @@ -159,16 +134,16 @@ func GetEthAuthKeyFromConfig() (public, private []byte, err error) { func GetPrivateEthAuthKey(fileName string) (key []byte, err error) { key, err = utils.ReadKeyFromPemFile(fileName, utils.PrivateKey) if err != nil { - log.Error(err) + return nil, err } - return + return key, nil } // GetPublicEthAuthKey returns the public key from the file func GetPublicEthAuthKey(fileName string) (key []byte, err error) { key, err = utils.ReadKeyFromPemFile(fileName, utils.PublicKey) if err != nil { - log.Error(err) + return nil, err } - return + return key, nil } diff --git a/keytools/secp256k1/secp256k1_test.go b/keytools/secp256k1/secp256k1_test.go index fe567e052..43458b380 100644 --- a/keytools/secp256k1/secp256k1_test.go +++ b/keytools/secp256k1/secp256k1_test.go @@ -16,12 +16,16 @@ import ( const MaxMsgLen = 32 +var ctx = map[string]interface{}{} +var cfg config.Configuration + func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &config.Bootstrapper{}, &testlogging.TestLoggingBootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) result := m.Run() os.Exit(result) } @@ -203,59 +207,30 @@ func TestGetAddress(t *testing.T) { } func TestGetEthAuthKeyFromConfig(t *testing.T) { - pub := config.Config.V.Get("keys.ethauth.publicKey") - pri := config.Config.V.Get("keys.ethauth.privateKey") + pub := cfg.Get("keys.ethauth.publicKey") + pri := cfg.Get("keys.ethauth.privateKey") // bad public key path - config.Config.V.Set("keys.ethauth.publicKey", "bad path") - pubK, priK, err := GetEthAuthKeyFromConfig() + cfg.Set("keys.ethauth.publicKey", "bad path") + pubK, priK, err := GetEthAuthKey(cfg.GetEthAuthKeyPair()) assert.Error(t, err) assert.Nil(t, priK) assert.Nil(t, pubK) assert.Contains(t, err.Error(), "failed to read public key") - config.Config.V.Set("keys.ethauth.publicKey", pub) + cfg.Set("keys.ethauth.publicKey", pub) // bad private key path - config.Config.V.Set("keys.ethauth.privateKey", "bad path") - pubK, priK, err = GetEthAuthKeyFromConfig() + cfg.Set("keys.ethauth.privateKey", "bad path") + pubK, priK, err = GetEthAuthKey(cfg.GetEthAuthKeyPair()) assert.Error(t, err) assert.Nil(t, priK) assert.Nil(t, pubK) assert.Contains(t, err.Error(), "failed to read private key") - config.Config.V.Set("keys.ethauth.privateKey", pri) + cfg.Set("keys.ethauth.privateKey", pri) // success - pubK, priK, err = GetEthAuthKeyFromConfig() + pubK, priK, err = GetEthAuthKey(cfg.GetEthAuthKeyPair()) assert.Nil(t, err) assert.NotNil(t, pubK) assert.NotNil(t, priK) } - -func TestGetIDConfig(t *testing.T) { - pub := config.Config.V.Get("keys.ethauth.publicKey") - - // failed keys - config.Config.V.Set("keys.ethauth.publicKey", "bad path") - id, err := GetIDConfig() - assert.Error(t, err) - assert.Nil(t, id) - assert.Contains(t, err.Error(), "failed to get eth keys") - config.Config.V.Set("keys.ethauth.publicKey", pub) - - // failed identity - gID := config.Config.V.Get("identityId") - config.Config.V.Set("identityId", "bad id") - id, err = GetIDConfig() - assert.Error(t, err) - assert.Nil(t, id) - assert.Contains(t, err.Error(), "can't read identityId from config") - config.Config.V.Set("identityId", gID) - - // success - id, err = GetIDConfig() - assert.Nil(t, err) - assert.NotNil(t, id) - nID, err := config.Config.GetIdentityID() - assert.Nil(t, err) - assert.Equal(t, id.ID, nID) -} diff --git a/keytools/sign.go b/keytools/sign.go index 874af8a28..1b8977d95 100644 --- a/keytools/sign.go +++ b/keytools/sign.go @@ -1,31 +1,29 @@ package keytools import ( - "fmt" "strings" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/keytools/secp256k1" ) +// SignMessage signs the message using the private key as the curveType provided. +// if ethereumSign is true, then the signature format is specific to ethereum. func SignMessage(privateKey, message []byte, curveType string, ethereumSign bool) ([]byte, error) { - curveType = strings.ToLower(curveType) - switch curveType { - case CurveSecp256K1: msg := make([]byte, MaxMsgLen) copy(msg, message) if ethereumSign { return secp256k1.SignEthereum(msg, privateKey) } - return secp256k1.Sign(msg, privateKey) + return secp256k1.Sign(msg, privateKey) case CurveEd25519: - return []byte(""), fmt.Errorf("curve ed25519 not supported yet") - + return nil, errors.New("curve ed25519 not supported yet") default: - return []byte(""), fmt.Errorf("curve %s not supported", curveType) + return nil, errors.New("curve %s not supported", curveType) } } diff --git a/keytools/sign_test.go b/keytools/sign_test.go index f44583897..a4f392d85 100644 --- a/keytools/sign_test.go +++ b/keytools/sign_test.go @@ -32,7 +32,6 @@ func TestSignMessage(t *testing.T) { os.Remove(privateKeyFile) assert.True(t, correct, "signature or verification didn't work correctly") - } func TestSignAndVerifyMessageEthereum(t *testing.T) { @@ -65,5 +64,4 @@ func TestSignAndVerifyMessageEthereum(t *testing.T) { os.Remove(privateKeyFile) assert.True(t, correct, "signature or verification didn't work correctly") - } diff --git a/keytools/verify.go b/keytools/verify.go index 14c9f346a..37f318d76 100644 --- a/keytools/verify.go +++ b/keytools/verify.go @@ -5,13 +5,13 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ) +// VerifyMessage verifies message using the public key as per the curve type. +// if ethereumVerify is true, ethereum specific verification is done func VerifyMessage(publicKey, message []byte, signature []byte, curveType string, ethereumVerify bool) bool { - signatureBytes := make([]byte, len(signature)) copy(signatureBytes, signature) switch curveType { - case CurveSecp256K1: msg := make([]byte, MaxMsgLen) copy(msg, message) @@ -19,13 +19,11 @@ func VerifyMessage(publicKey, message []byte, signature []byte, curveType string address := secp256k1.GetAddress(publicKey) return secp256k1.VerifySignatureWithAddress(address, hexutil.Encode(signatureBytes), msg) } - return secp256k1.VerifySignature(publicKey, msg, signatureBytes) + return secp256k1.VerifySignature(publicKey, msg, signatureBytes) case CurveEd25519: return false - default: return false } - } diff --git a/keytools/verify_test.go b/keytools/verify_test.go index 7a220d7c1..d87fec21d 100644 --- a/keytools/verify_test.go +++ b/keytools/verify_test.go @@ -11,7 +11,6 @@ import ( ) func TestVerifyMessageED25519(t *testing.T) { - publicKeyFile := "publicKey" privateKeyFile := "privateKey" testMsg := "test" @@ -23,7 +22,5 @@ func TestVerifyMessageED25519(t *testing.T) { assert.NotNil(t, err) os.Remove(publicKeyFile) os.Remove(privateKeyFile) - assert.True(t, len(signature) == 0, "verify ed25519 is not implemented yet and should not work") - } diff --git a/main.go b/main.go deleted file mode 100644 index 8033083b8..000000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/centrifuge/go-centrifuge/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/nft/bootstrapper.go b/nft/bootstrapper.go index 15104cc1a..dba1ecf93 100644 --- a/nft/bootstrapper.go +++ b/nft/bootstrapper.go @@ -1,35 +1,50 @@ package nft import ( - "errors" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/queue" ) -type Bootstrapper struct { -} +// BootstrappedPayObService is the key to PaymentObligationService in bootstrap context. +const BootstrappedPayObService = "BootstrappedPayObService" + +// Bootstrapper implements bootstrap.Bootstrapper. +type Bootstrapper struct{} // Bootstrap initializes the payment obligation contract -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - if _, ok := context[bootstrap.BootstrappedConfig]; !ok { +func (*Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + if _, ok := ctx[bootstrap.BootstrappedConfig]; !ok { return errors.New("config hasn't been initialized") } - if _, ok := context[bootstrap.BootstrappedEthereumClient]; !ok { + cfg := ctx[bootstrap.BootstrappedConfig].(Config) + + if _, ok := ctx[ethereum.BootstrappedEthereumClient]; !ok { return errors.New("ethereum client hasn't been initialized") } - contract, err := getPaymentObligationContract() - if err != nil { - return err + registry, ok := ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("service registry not initialised") } - setPaymentObligation(NewEthereumPaymentObligation(contract, identity.IDService, ethereum.GetConnection(), config.Config)) - return nil -} -func getPaymentObligationContract() (*EthereumPaymentObligationContract, error) { - client := ethereum.GetConnection() - return NewEthereumPaymentObligationContract(config.Config.GetContractAddress("paymentObligation"), client.GetClient()) + idService, ok := ctx[identity.BootstrappedIDService].(identity.Service) + if !ok { + return errors.New("identity service not initialised") + } + + if _, ok := ctx[bootstrap.BootstrappedQueueServer]; !ok { + return errors.New("queue hasn't been initialized") + } + queueSrv := ctx[bootstrap.BootstrappedQueueServer].(*queue.Server) + + ctx[BootstrappedPayObService] = newEthereumPaymentObligation(registry, idService, ethereum.GetClient(), cfg, queueSrv, setupMintListener, bindContract) + // queue task + task := newMintingConfirmationTask(cfg.GetEthereumContextWaitTimeout(), ethereum.DefaultWaitForTransactionMiningContext) + queueSrv.RegisterTaskType(task.TaskTypeName(), task) + return nil } diff --git a/nft/ethereum_payment_obligation.go b/nft/ethereum_payment_obligation.go index fec23cf71..c1ff084b0 100644 --- a/nft/ethereum_payment_obligation.go +++ b/nft/ethereum_payment_obligation.go @@ -1,116 +1,175 @@ package nft import ( + "context" + "encoding/hex" "math/big" "github.com/centrifuge/go-centrifuge/anchors" "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/queue" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/precise-proofs/proofs/proto" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/go-errors/errors" + + "time" + logging "github.com/ipfs/go-log" ) var log = logging.Logger("nft") -var po *ethereumPaymentObligation - -func setPaymentObligation(s *ethereumPaymentObligation) { - po = s -} - -func GetPaymentObligation() *ethereumPaymentObligation { - return po -} - // Config is an interface to configurations required by nft package type Config interface { GetIdentityID() ([]byte, error) GetEthereumDefaultAccountName() string + GetContractAddress(address string) common.Address + GetEthereumContextWaitTimeout() time.Duration } // ethereumPaymentObligationContract is an abstraction over the contract code to help in mocking it out type ethereumPaymentObligationContract interface { // Mint method abstracts Mint method on the contract - Mint(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values [3]string, _salts [3][32]byte, _proofs [3][][32]byte) (*types.Transaction, error) + Mint(opts *bind.TransactOpts, to common.Address, tokenID *big.Int, tokenURI string, anchorID *big.Int, merkleRoot [32]byte, values []string, salts [][32]byte, proofs [][][32]byte) (*types.Transaction, error) } // ethereumPaymentObligation handles all interactions related to minting of NFTs for payment obligations on Ethereum type ethereumPaymentObligation struct { - paymentObligation ethereumPaymentObligationContract + registry *documents.ServiceRegistry identityService identity.Service - ethClient ethereum.EthereumClient + ethClient ethereum.Client config Config + queue *queue.Server + setupMintListener func(config Config, queue *queue.Server, tokenID *big.Int, registryAddress string) (confirmations chan *watchTokenMinted, err error) + bindContract func(address common.Address, client ethereum.Client) (*EthereumPaymentObligationContract, error) } -// NewEthereumPaymentObligation creates ethereumPaymentObligation given the parameters -func NewEthereumPaymentObligation(paymentObligation ethereumPaymentObligationContract, identityService identity.Service, ethClient ethereum.EthereumClient, config Config) *ethereumPaymentObligation { - return ðereumPaymentObligation{paymentObligation: paymentObligation, identityService: identityService, ethClient: ethClient, config: config} +// newEthereumPaymentObligation creates ethereumPaymentObligation given the parameters +func newEthereumPaymentObligation(registry *documents.ServiceRegistry, identityService identity.Service, ethClient ethereum.Client, config Config, + queue *queue.Server, + setupMintListener func(config Config, queue *queue.Server, tokenID *big.Int, registryAddress string) (confirmations chan *watchTokenMinted, err error), bindContract func(address common.Address, client ethereum.Client) (*EthereumPaymentObligationContract, error)) *ethereumPaymentObligation { + return ðereumPaymentObligation{ + registry: registry, + identityService: identityService, + ethClient: ethClient, + config: config, + setupMintListener: setupMintListener, + bindContract: bindContract, + queue: queue, + } } -// MintNFT mints an NFT -func (s *ethereumPaymentObligation) MintNFT(documentID []byte, docType, registryAddress, depositAddress string, proofFields []string) (string, error) { - docService, err := getDocumentService(docType) +func (s *ethereumPaymentObligation) prepareMintRequest(documentID []byte, depositAddress string, proofFields []string) (*MintRequest, error) { + docService, err := s.registry.FindService(documentID) if err != nil { - return "", err + return nil, err } model, err := docService.GetCurrentVersion(documentID) if err != nil { - return "", err + return nil, err } corDoc, err := model.PackCoreDocument() if err != nil { - return "", err + return nil, err } proofs, err := docService.CreateProofs(documentID, proofFields) if err != nil { - return "", err + return nil, err } - toAddress, err := s.getIdentityAddress() + toAddress := common.HexToAddress(depositAddress) + + anchorID, err := anchors.ToAnchorID(corDoc.CurrentVersion) if err != nil { - return "", nil + return nil, nil } - anchorID, err := anchors.NewAnchorID(corDoc.CurrentVersion) + rootHash, err := anchors.ToDocumentRoot(corDoc.DocumentRoot) if err != nil { - return "", nil + return nil, nil } - rootHash, err := anchors.NewDocRoot(corDoc.DocumentRoot) + requestData, err := NewMintRequest(toAddress, anchorID, proofs.FieldProofs, rootHash) if err != nil { - return "", nil + return nil, err } - requestData, err := NewMintRequest(toAddress, anchorID, proofs.FieldProofs, rootHash) + return requestData, nil + +} + +// MintNFT mints an NFT +func (s *ethereumPaymentObligation) MintNFT(documentID []byte, registryAddress, depositAddress string, proofFields []string) (<-chan *watchTokenMinted, error) { + + requestData, err := s.prepareMintRequest(documentID, depositAddress, proofFields) if err != nil { - return "", err + return nil, errors.New("failed to prepare mint request: %v", err) } opts, err := s.ethClient.GetTxOpts(s.config.GetEthereumDefaultAccountName()) if err != nil { - return "", err + return nil, err + } + + contract, err := s.bindContract(common.HexToAddress(registryAddress), s.ethClient) + + if err != nil { + return nil, err } - err = s.sendMintTransaction(s.paymentObligation, opts, requestData) + watch, err := s.setupMintListener(s.config, s.queue, requestData.TokenID, registryAddress) if err != nil { - return "", err + return nil, err } - return requestData.TokenID.String(), nil + err = s.sendMintTransaction(contract, opts, requestData) + if err != nil { + return nil, err + } + + return watch, nil +} + +// setUpMintEventListener sets up the listened for the "PaymentObligationMinted" event to notify the upstream code +// about successful minting of an NFt +func setupMintListener(config Config, qs *queue.Server, tokenID *big.Int, registryAddress string) (confirmations chan *watchTokenMinted, err error) { + confirmations = make(chan *watchTokenMinted) + conn := ethereum.GetClient() + + h, err := conn.GetEthClient().HeaderByNumber(context.Background(), nil) + if err != nil { + return nil, err + } + asyncRes, err := qs.EnqueueJob(mintingConfirmationTaskName, map[string]interface{}{ + tokenIDParam: hex.EncodeToString(tokenID.Bytes()), + queue.BlockHeightParam: h.Number.Uint64(), + registryAddressParam: registryAddress, + }) + if err != nil { + return nil, err + } + + go waitAndRouteNFTApprovedEvent(config.GetEthereumContextWaitTimeout(), asyncRes, tokenID, confirmations) + return confirmations, nil +} + +// waitAndRouteNFTApprovedEvent notifies the confirmations channel whenever the key has been added to the identity and has been noted as Ethereum event +func waitAndRouteNFTApprovedEvent(timeout time.Duration, asyncRes queue.TaskResult, tokenID *big.Int, confirmations chan<- *watchTokenMinted) { + _, err := asyncRes.Get(timeout) + confirmations <- &watchTokenMinted{tokenID, err} } // sendMintTransaction sends the actual transaction to mint the NFT -func (s *ethereumPaymentObligation) sendMintTransaction(contract ethereumPaymentObligationContract, opts *bind.TransactOpts, requestData *MintRequest) (err error) { +func (s *ethereumPaymentObligation) sendMintTransaction(contract ethereumPaymentObligationContract, opts *bind.TransactOpts, requestData *MintRequest) error { tx, err := s.ethClient.SubmitTransactionWithRetries(contract.Mint, opts, requestData.To, requestData.TokenID, requestData.TokenURI, requestData.AnchorID, requestData.MerkleRoot, requestData.Values, requestData.Salts, requestData.Proofs) if err != nil { @@ -119,7 +178,7 @@ func (s *ethereumPaymentObligation) sendMintTransaction(contract ethereumPayment log.Infof("Sent off tx to mint [tokenID: %x, anchor: %x, registry: %x] to payment obligation contract. Ethereum transaction hash [%x] and Nonce [%v] and Check [%v]", requestData.TokenID, requestData.AnchorID, requestData.To, tx.Hash(), tx.Nonce(), tx.CheckNonce()) log.Infof("Transfer pending: 0x%x\n", tx.Hash()) - return + return nil } func (s *ethereumPaymentObligation) getIdentityAddress() (common.Address, error) { @@ -159,18 +218,21 @@ type MintRequest struct { MerkleRoot [32]byte // Values are the values of the leafs that is being proved Will be converted to string and concatenated for proof verification as outlined in precise-proofs library. - Values [3]string + Values []string // salts are the salts for the field that is being proved Will be concatenated for proof verification as outlined in precise-proofs library. - Salts [3][32]byte + Salts [][32]byte // Proofs are the documents proofs that are needed - Proofs [3][][32]byte + Proofs [][][32]byte } // NewMintRequest converts the parameters and returns a struct with needed parameter for minting func NewMintRequest(to common.Address, anchorID anchors.AnchorID, proofs []*proofspb.Proof, rootHash [32]byte) (*MintRequest, error) { - tokenID := utils.ByteSliceToBigInt(utils.RandomSlice(256)) + + // tokenID is uint256 in Solidity (256 bits | max value is 2^256-1) + // tokenID should be random 32 bytes (32 byte = 256 bits) + tokenID := utils.ByteSliceToBigInt(utils.RandomSlice(32)) tokenURI := "http:=//www.centrifuge.io/DUMMY_URI_SERVICE" proofData, err := createProofData(proofs) if err != nil { @@ -189,18 +251,15 @@ func NewMintRequest(to common.Address, anchorID anchors.AnchorID, proofs []*proo } type proofData struct { - Values [3]string - Salts [3][32]byte - Proofs [3][][32]byte + Values []string + Salts [][32]byte + Proofs [][][32]byte } func createProofData(proofspb []*proofspb.Proof) (*proofData, error) { - if len(proofspb) > 3 { - return nil, errors.New("no more than 3 field proofs are accepted") - } - var values [3]string - var salts [3][32]byte - var proofs [3][][32]byte + var values = make([]string, len(proofspb)) + var salts = make([][32]byte, len(proofspb)) + var proofs = make([][][32]byte, len(proofspb)) for i, p := range proofspb { values[i] = p.Value @@ -233,10 +292,6 @@ func convertProofProperty(sortedHashes [][]byte) ([][32]byte, error) { return property, nil } -func getDocumentService(documentType string) (documents.Service, error) { - docService, err := documents.GetRegistryInstance().LocateService(documentType) - if err != nil { - return nil, err - } - return docService, nil +func bindContract(address common.Address, client ethereum.Client) (*EthereumPaymentObligationContract, error) { + return NewEthereumPaymentObligationContract(address, client.GetEthClient()) } diff --git a/nft/ethereum_payment_obligation_contract.go b/nft/ethereum_payment_obligation_contract.go index a2018de38..9338fbf35 100644 --- a/nft/ethereum_payment_obligation_contract.go +++ b/nft/ethereum_payment_obligation_contract.go @@ -16,7 +16,7 @@ import ( ) // EthereumPaymentObligationContractABI is the input ABI used to generate the binding from. -const EthereumPaymentObligationContractABI = "[{\"constant\":true,\"inputs\":[{\"name\":\"_interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"getApproved\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"InterfaceId_ERC165\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes4\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_index\",\"type\":\"uint256\"}],\"name\":\"tokenOfOwnerByIndex\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"exists\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_index\",\"type\":\"uint256\"}],\"name\":\"tokenByIndex\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"anchorRegistry\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"ownerOf\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_approved\",\"type\":\"bool\"}],\"name\":\"setApprovalForAll\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"},{\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"tokenURI\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_operator\",\"type\":\"address\"}],\"name\":\"isApprovedForAll\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"},{\"name\":\"_symbol\",\"type\":\"string\"},{\"name\":\"_anchorRegistry\",\"type\":\"address\"},{\"name\":\"_identityRegistry\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"tokenURI\",\"type\":\"string\"}],\"name\":\"PaymentObligationMinted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_approved\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_operator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_approved\",\"type\":\"bool\"}],\"name\":\"ApprovalForAll\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"},{\"name\":\"_tokenURI\",\"type\":\"string\"},{\"name\":\"_anchorId\",\"type\":\"uint256\"},{\"name\":\"_merkleRoot\",\"type\":\"bytes32\"},{\"name\":\"_values\",\"type\":\"string[3]\"},{\"name\":\"_salts\",\"type\":\"bytes32[3]\"},{\"name\":\"_proofs\",\"type\":\"bytes32[][3]\"}],\"name\":\"mint\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"getTokenDetails\",\"outputs\":[{\"name\":\"grossAmount\",\"type\":\"string\"},{\"name\":\"currency\",\"type\":\"string\"},{\"name\":\"dueDate\",\"type\":\"string\"},{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" +const EthereumPaymentObligationContractABI = "[{\"constant\":true,\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"getApproved\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"}],\"name\":\"initialize\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"anchorRegistry\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"ownerOf\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"initialize\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"setApprovalForAll\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\"},{\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"mandatoryFields\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"tokenURI\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"isApprovedForAll\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"_anchorRegistry\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"tokenURI\",\"type\":\"string\"}],\"name\":\"PaymentObligationMinted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"approved\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"ApprovalForAll\",\"type\":\"event\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_tokenId\",\"type\":\"uint256\"},{\"name\":\"_tokenURI\",\"type\":\"string\"},{\"name\":\"_anchorId\",\"type\":\"uint256\"},{\"name\":\"_merkleRoot\",\"type\":\"bytes32\"},{\"name\":\"_values\",\"type\":\"string[]\"},{\"name\":\"_salts\",\"type\":\"bytes32[]\"},{\"name\":\"_proofs\",\"type\":\"bytes32[][]\"}],\"name\":\"mint\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_tokenId\",\"type\":\"uint256\"}],\"name\":\"getTokenDetails\",\"outputs\":[{\"name\":\"grossAmount\",\"type\":\"string\"},{\"name\":\"currency\",\"type\":\"string\"},{\"name\":\"dueDate\",\"type\":\"string\"},{\"name\":\"anchorId\",\"type\":\"uint256\"},{\"name\":\"documentRoot\",\"type\":\"bytes32\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" // EthereumPaymentObligationContract is an auto generated Go binding around an Ethereum contract. type EthereumPaymentObligationContract struct { @@ -160,32 +160,6 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTrans return _EthereumPaymentObligationContract.Contract.contract.Transact(opts, method, params...) } -// InterfaceIdERC165 is a free data retrieval call binding the contract method 0x19fa8f50. -// -// Solidity: function InterfaceId_ERC165() constant returns(bytes4) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) InterfaceIdERC165(opts *bind.CallOpts) ([4]byte, error) { - var ( - ret0 = new([4]byte) - ) - out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "InterfaceId_ERC165") - return *ret0, err -} - -// InterfaceIdERC165 is a free data retrieval call binding the contract method 0x19fa8f50. -// -// Solidity: function InterfaceId_ERC165() constant returns(bytes4) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) InterfaceIdERC165() ([4]byte, error) { - return _EthereumPaymentObligationContract.Contract.InterfaceIdERC165(&_EthereumPaymentObligationContract.CallOpts) -} - -// InterfaceIdERC165 is a free data retrieval call binding the contract method 0x19fa8f50. -// -// Solidity: function InterfaceId_ERC165() constant returns(bytes4) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) InterfaceIdERC165() ([4]byte, error) { - return _EthereumPaymentObligationContract.Contract.InterfaceIdERC165(&_EthereumPaymentObligationContract.CallOpts) -} - // AnchorRegistry is a free data retrieval call binding the contract method 0x5a180c0a. // // Solidity: function anchorRegistry() constant returns(address) @@ -214,80 +188,54 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCalle // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. // -// Solidity: function balanceOf(_owner address) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) BalanceOf(opts *bind.CallOpts, _owner common.Address) (*big.Int, error) { +// Solidity: function balanceOf(owner address) constant returns(uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) BalanceOf(opts *bind.CallOpts, owner common.Address) (*big.Int, error) { var ( ret0 = new(*big.Int) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "balanceOf", _owner) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "balanceOf", owner) return *ret0, err } // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. // -// Solidity: function balanceOf(_owner address) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) BalanceOf(_owner common.Address) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.BalanceOf(&_EthereumPaymentObligationContract.CallOpts, _owner) +// Solidity: function balanceOf(owner address) constant returns(uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) BalanceOf(owner common.Address) (*big.Int, error) { + return _EthereumPaymentObligationContract.Contract.BalanceOf(&_EthereumPaymentObligationContract.CallOpts, owner) } // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. // -// Solidity: function balanceOf(_owner address) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) BalanceOf(_owner common.Address) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.BalanceOf(&_EthereumPaymentObligationContract.CallOpts, _owner) -} - -// Exists is a free data retrieval call binding the contract method 0x4f558e79. -// -// Solidity: function exists(_tokenId uint256) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) Exists(opts *bind.CallOpts, _tokenId *big.Int) (bool, error) { - var ( - ret0 = new(bool) - ) - out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "exists", _tokenId) - return *ret0, err -} - -// Exists is a free data retrieval call binding the contract method 0x4f558e79. -// -// Solidity: function exists(_tokenId uint256) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Exists(_tokenId *big.Int) (bool, error) { - return _EthereumPaymentObligationContract.Contract.Exists(&_EthereumPaymentObligationContract.CallOpts, _tokenId) -} - -// Exists is a free data retrieval call binding the contract method 0x4f558e79. -// -// Solidity: function exists(_tokenId uint256) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) Exists(_tokenId *big.Int) (bool, error) { - return _EthereumPaymentObligationContract.Contract.Exists(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function balanceOf(owner address) constant returns(uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) BalanceOf(owner common.Address) (*big.Int, error) { + return _EthereumPaymentObligationContract.Contract.BalanceOf(&_EthereumPaymentObligationContract.CallOpts, owner) } // GetApproved is a free data retrieval call binding the contract method 0x081812fc. // -// Solidity: function getApproved(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) GetApproved(opts *bind.CallOpts, _tokenId *big.Int) (common.Address, error) { +// Solidity: function getApproved(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) GetApproved(opts *bind.CallOpts, tokenId *big.Int) (common.Address, error) { var ( ret0 = new(common.Address) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "getApproved", _tokenId) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "getApproved", tokenId) return *ret0, err } // GetApproved is a free data retrieval call binding the contract method 0x081812fc. // -// Solidity: function getApproved(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) GetApproved(_tokenId *big.Int) (common.Address, error) { - return _EthereumPaymentObligationContract.Contract.GetApproved(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function getApproved(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) GetApproved(tokenId *big.Int) (common.Address, error) { + return _EthereumPaymentObligationContract.Contract.GetApproved(&_EthereumPaymentObligationContract.CallOpts, tokenId) } // GetApproved is a free data retrieval call binding the contract method 0x081812fc. // -// Solidity: function getApproved(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) GetApproved(_tokenId *big.Int) (common.Address, error) { - return _EthereumPaymentObligationContract.Contract.GetApproved(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function getApproved(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) GetApproved(tokenId *big.Int) (common.Address, error) { + return _EthereumPaymentObligationContract.Contract.GetApproved(&_EthereumPaymentObligationContract.CallOpts, tokenId) } // GetTokenDetails is a free data retrieval call binding the contract method 0xc1e03728. @@ -340,28 +288,54 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCalle // IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. // -// Solidity: function isApprovedForAll(_owner address, _operator address) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) IsApprovedForAll(opts *bind.CallOpts, _owner common.Address, _operator common.Address) (bool, error) { +// Solidity: function isApprovedForAll(owner address, operator address) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) IsApprovedForAll(opts *bind.CallOpts, owner common.Address, operator common.Address) (bool, error) { var ( ret0 = new(bool) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "isApprovedForAll", _owner, _operator) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "isApprovedForAll", owner, operator) return *ret0, err } // IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. // -// Solidity: function isApprovedForAll(_owner address, _operator address) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) IsApprovedForAll(_owner common.Address, _operator common.Address) (bool, error) { - return _EthereumPaymentObligationContract.Contract.IsApprovedForAll(&_EthereumPaymentObligationContract.CallOpts, _owner, _operator) +// Solidity: function isApprovedForAll(owner address, operator address) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) IsApprovedForAll(owner common.Address, operator common.Address) (bool, error) { + return _EthereumPaymentObligationContract.Contract.IsApprovedForAll(&_EthereumPaymentObligationContract.CallOpts, owner, operator) } // IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. // -// Solidity: function isApprovedForAll(_owner address, _operator address) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) IsApprovedForAll(_owner common.Address, _operator common.Address) (bool, error) { - return _EthereumPaymentObligationContract.Contract.IsApprovedForAll(&_EthereumPaymentObligationContract.CallOpts, _owner, _operator) +// Solidity: function isApprovedForAll(owner address, operator address) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) IsApprovedForAll(owner common.Address, operator common.Address) (bool, error) { + return _EthereumPaymentObligationContract.Contract.IsApprovedForAll(&_EthereumPaymentObligationContract.CallOpts, owner, operator) +} + +// MandatoryFields is a free data retrieval call binding the contract method 0xc4ac61f7. +// +// Solidity: function mandatoryFields( uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) MandatoryFields(opts *bind.CallOpts, arg0 *big.Int) (string, error) { + var ( + ret0 = new(string) + ) + out := ret0 + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "mandatoryFields", arg0) + return *ret0, err +} + +// MandatoryFields is a free data retrieval call binding the contract method 0xc4ac61f7. +// +// Solidity: function mandatoryFields( uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) MandatoryFields(arg0 *big.Int) (string, error) { + return _EthereumPaymentObligationContract.Contract.MandatoryFields(&_EthereumPaymentObligationContract.CallOpts, arg0) +} + +// MandatoryFields is a free data retrieval call binding the contract method 0xc4ac61f7. +// +// Solidity: function mandatoryFields( uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) MandatoryFields(arg0 *big.Int) (string, error) { + return _EthereumPaymentObligationContract.Contract.MandatoryFields(&_EthereumPaymentObligationContract.CallOpts, arg0) } // Name is a free data retrieval call binding the contract method 0x06fdde03. @@ -392,54 +366,54 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCalle // OwnerOf is a free data retrieval call binding the contract method 0x6352211e. // -// Solidity: function ownerOf(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) OwnerOf(opts *bind.CallOpts, _tokenId *big.Int) (common.Address, error) { +// Solidity: function ownerOf(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) OwnerOf(opts *bind.CallOpts, tokenId *big.Int) (common.Address, error) { var ( ret0 = new(common.Address) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "ownerOf", _tokenId) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "ownerOf", tokenId) return *ret0, err } // OwnerOf is a free data retrieval call binding the contract method 0x6352211e. // -// Solidity: function ownerOf(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) OwnerOf(_tokenId *big.Int) (common.Address, error) { - return _EthereumPaymentObligationContract.Contract.OwnerOf(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function ownerOf(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) OwnerOf(tokenId *big.Int) (common.Address, error) { + return _EthereumPaymentObligationContract.Contract.OwnerOf(&_EthereumPaymentObligationContract.CallOpts, tokenId) } // OwnerOf is a free data retrieval call binding the contract method 0x6352211e. // -// Solidity: function ownerOf(_tokenId uint256) constant returns(address) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) OwnerOf(_tokenId *big.Int) (common.Address, error) { - return _EthereumPaymentObligationContract.Contract.OwnerOf(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function ownerOf(tokenId uint256) constant returns(address) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) OwnerOf(tokenId *big.Int) (common.Address, error) { + return _EthereumPaymentObligationContract.Contract.OwnerOf(&_EthereumPaymentObligationContract.CallOpts, tokenId) } // SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. // -// Solidity: function supportsInterface(_interfaceId bytes4) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) SupportsInterface(opts *bind.CallOpts, _interfaceId [4]byte) (bool, error) { +// Solidity: function supportsInterface(interfaceId bytes4) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { var ( ret0 = new(bool) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "supportsInterface", _interfaceId) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "supportsInterface", interfaceId) return *ret0, err } // SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. // -// Solidity: function supportsInterface(_interfaceId bytes4) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SupportsInterface(_interfaceId [4]byte) (bool, error) { - return _EthereumPaymentObligationContract.Contract.SupportsInterface(&_EthereumPaymentObligationContract.CallOpts, _interfaceId) +// Solidity: function supportsInterface(interfaceId bytes4) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _EthereumPaymentObligationContract.Contract.SupportsInterface(&_EthereumPaymentObligationContract.CallOpts, interfaceId) } // SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. // -// Solidity: function supportsInterface(_interfaceId bytes4) constant returns(bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) SupportsInterface(_interfaceId [4]byte) (bool, error) { - return _EthereumPaymentObligationContract.Contract.SupportsInterface(&_EthereumPaymentObligationContract.CallOpts, _interfaceId) +// Solidity: function supportsInterface(interfaceId bytes4) constant returns(bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _EthereumPaymentObligationContract.Contract.SupportsInterface(&_EthereumPaymentObligationContract.CallOpts, interfaceId) } // Symbol is a free data retrieval call binding the contract method 0x95d89b41. @@ -468,213 +442,156 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCalle return _EthereumPaymentObligationContract.Contract.Symbol(&_EthereumPaymentObligationContract.CallOpts) } -// TokenByIndex is a free data retrieval call binding the contract method 0x4f6ccce7. -// -// Solidity: function tokenByIndex(_index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) TokenByIndex(opts *bind.CallOpts, _index *big.Int) (*big.Int, error) { - var ( - ret0 = new(*big.Int) - ) - out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "tokenByIndex", _index) - return *ret0, err -} - -// TokenByIndex is a free data retrieval call binding the contract method 0x4f6ccce7. -// -// Solidity: function tokenByIndex(_index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TokenByIndex(_index *big.Int) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TokenByIndex(&_EthereumPaymentObligationContract.CallOpts, _index) -} - -// TokenByIndex is a free data retrieval call binding the contract method 0x4f6ccce7. -// -// Solidity: function tokenByIndex(_index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) TokenByIndex(_index *big.Int) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TokenByIndex(&_EthereumPaymentObligationContract.CallOpts, _index) -} - -// TokenOfOwnerByIndex is a free data retrieval call binding the contract method 0x2f745c59. -// -// Solidity: function tokenOfOwnerByIndex(_owner address, _index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) TokenOfOwnerByIndex(opts *bind.CallOpts, _owner common.Address, _index *big.Int) (*big.Int, error) { - var ( - ret0 = new(*big.Int) - ) - out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "tokenOfOwnerByIndex", _owner, _index) - return *ret0, err -} - -// TokenOfOwnerByIndex is a free data retrieval call binding the contract method 0x2f745c59. -// -// Solidity: function tokenOfOwnerByIndex(_owner address, _index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TokenOfOwnerByIndex(_owner common.Address, _index *big.Int) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TokenOfOwnerByIndex(&_EthereumPaymentObligationContract.CallOpts, _owner, _index) -} - -// TokenOfOwnerByIndex is a free data retrieval call binding the contract method 0x2f745c59. -// -// Solidity: function tokenOfOwnerByIndex(_owner address, _index uint256) constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) TokenOfOwnerByIndex(_owner common.Address, _index *big.Int) (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TokenOfOwnerByIndex(&_EthereumPaymentObligationContract.CallOpts, _owner, _index) -} - // TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. // -// Solidity: function tokenURI(_tokenId uint256) constant returns(string) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) TokenURI(opts *bind.CallOpts, _tokenId *big.Int) (string, error) { +// Solidity: function tokenURI(tokenId uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) TokenURI(opts *bind.CallOpts, tokenId *big.Int) (string, error) { var ( ret0 = new(string) ) out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "tokenURI", _tokenId) + err := _EthereumPaymentObligationContract.contract.Call(opts, out, "tokenURI", tokenId) return *ret0, err } // TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. // -// Solidity: function tokenURI(_tokenId uint256) constant returns(string) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TokenURI(_tokenId *big.Int) (string, error) { - return _EthereumPaymentObligationContract.Contract.TokenURI(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function tokenURI(tokenId uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TokenURI(tokenId *big.Int) (string, error) { + return _EthereumPaymentObligationContract.Contract.TokenURI(&_EthereumPaymentObligationContract.CallOpts, tokenId) } // TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. // -// Solidity: function tokenURI(_tokenId uint256) constant returns(string) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) TokenURI(_tokenId *big.Int) (string, error) { - return _EthereumPaymentObligationContract.Contract.TokenURI(&_EthereumPaymentObligationContract.CallOpts, _tokenId) +// Solidity: function tokenURI(tokenId uint256) constant returns(string) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) TokenURI(tokenId *big.Int) (string, error) { + return _EthereumPaymentObligationContract.Contract.TokenURI(&_EthereumPaymentObligationContract.CallOpts, tokenId) } -// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. // -// Solidity: function totalSupply() constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCaller) TotalSupply(opts *bind.CallOpts) (*big.Int, error) { - var ( - ret0 = new(*big.Int) - ) - out := ret0 - err := _EthereumPaymentObligationContract.contract.Call(opts, out, "totalSupply") - return *ret0, err +// Solidity: function approve(to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) Approve(opts *bind.TransactOpts, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.contract.Transact(opts, "approve", to, tokenId) } -// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. // -// Solidity: function totalSupply() constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TotalSupply() (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TotalSupply(&_EthereumPaymentObligationContract.CallOpts) +// Solidity: function approve(to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Approve(to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.Approve(&_EthereumPaymentObligationContract.TransactOpts, to, tokenId) } -// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. // -// Solidity: function totalSupply() constant returns(uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractCallerSession) TotalSupply() (*big.Int, error) { - return _EthereumPaymentObligationContract.Contract.TotalSupply(&_EthereumPaymentObligationContract.CallOpts) +// Solidity: function approve(to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) Approve(to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.Approve(&_EthereumPaymentObligationContract.TransactOpts, to, tokenId) } -// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// Initialize is a paid mutator transaction binding the contract method 0x8129fc1c. // -// Solidity: function approve(_to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) Approve(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.contract.Transact(opts, "approve", _to, _tokenId) +// Solidity: function initialize() returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) Initialize(opts *bind.TransactOpts) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.contract.Transact(opts, "initialize") } -// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// Initialize is a paid mutator transaction binding the contract method 0x8129fc1c. // -// Solidity: function approve(_to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Approve(_to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.Approve(&_EthereumPaymentObligationContract.TransactOpts, _to, _tokenId) +// Solidity: function initialize() returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Initialize() (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.Initialize(&_EthereumPaymentObligationContract.TransactOpts) } -// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// Initialize is a paid mutator transaction binding the contract method 0x8129fc1c. // -// Solidity: function approve(_to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) Approve(_to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.Approve(&_EthereumPaymentObligationContract.TransactOpts, _to, _tokenId) +// Solidity: function initialize() returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) Initialize() (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.Initialize(&_EthereumPaymentObligationContract.TransactOpts) } -// Mint is a paid mutator transaction binding the contract method 0xb279f8f1. +// Mint is a paid mutator transaction binding the contract method 0x093b02fe. // -// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[3], _salts bytes32[3], _proofs bytes32[][3]) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) Mint(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values [3]string, _salts [3][32]byte, _proofs [3][][32]byte) (*types.Transaction, error) { +// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[], _salts bytes32[], _proofs bytes32[][]) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) Mint(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values []string, _salts [][32]byte, _proofs [][][32]byte) (*types.Transaction, error) { return _EthereumPaymentObligationContract.contract.Transact(opts, "mint", _to, _tokenId, _tokenURI, _anchorId, _merkleRoot, _values, _salts, _proofs) } -// Mint is a paid mutator transaction binding the contract method 0xb279f8f1. +// Mint is a paid mutator transaction binding the contract method 0x093b02fe. // -// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[3], _salts bytes32[3], _proofs bytes32[][3]) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Mint(_to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values [3]string, _salts [3][32]byte, _proofs [3][][32]byte) (*types.Transaction, error) { +// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[], _salts bytes32[], _proofs bytes32[][]) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) Mint(_to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values []string, _salts [][32]byte, _proofs [][][32]byte) (*types.Transaction, error) { return _EthereumPaymentObligationContract.Contract.Mint(&_EthereumPaymentObligationContract.TransactOpts, _to, _tokenId, _tokenURI, _anchorId, _merkleRoot, _values, _salts, _proofs) } -// Mint is a paid mutator transaction binding the contract method 0xb279f8f1. +// Mint is a paid mutator transaction binding the contract method 0x093b02fe. // -// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[3], _salts bytes32[3], _proofs bytes32[][3]) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) Mint(_to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values [3]string, _salts [3][32]byte, _proofs [3][][32]byte) (*types.Transaction, error) { +// Solidity: function mint(_to address, _tokenId uint256, _tokenURI string, _anchorId uint256, _merkleRoot bytes32, _values string[], _salts bytes32[], _proofs bytes32[][]) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) Mint(_to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values []string, _salts [][32]byte, _proofs [][][32]byte) (*types.Transaction, error) { return _EthereumPaymentObligationContract.Contract.Mint(&_EthereumPaymentObligationContract.TransactOpts, _to, _tokenId, _tokenURI, _anchorId, _merkleRoot, _values, _salts, _proofs) } // SafeTransferFrom is a paid mutator transaction binding the contract method 0xb88d4fde. // -// Solidity: function safeTransferFrom(_from address, _to address, _tokenId uint256, _data bytes) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) SafeTransferFrom(opts *bind.TransactOpts, _from common.Address, _to common.Address, _tokenId *big.Int, _data []byte) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.contract.Transact(opts, "safeTransferFrom", _from, _to, _tokenId, _data) +// Solidity: function safeTransferFrom(from address, to address, tokenId uint256, _data bytes) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) SafeTransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, tokenId *big.Int, _data []byte) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.contract.Transact(opts, "safeTransferFrom", from, to, tokenId, _data) } // SafeTransferFrom is a paid mutator transaction binding the contract method 0xb88d4fde. // -// Solidity: function safeTransferFrom(_from address, _to address, _tokenId uint256, _data bytes) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SafeTransferFrom(_from common.Address, _to common.Address, _tokenId *big.Int, _data []byte) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.SafeTransferFrom(&_EthereumPaymentObligationContract.TransactOpts, _from, _to, _tokenId, _data) +// Solidity: function safeTransferFrom(from address, to address, tokenId uint256, _data bytes) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SafeTransferFrom(from common.Address, to common.Address, tokenId *big.Int, _data []byte) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.SafeTransferFrom(&_EthereumPaymentObligationContract.TransactOpts, from, to, tokenId, _data) } // SafeTransferFrom is a paid mutator transaction binding the contract method 0xb88d4fde. // -// Solidity: function safeTransferFrom(_from address, _to address, _tokenId uint256, _data bytes) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) SafeTransferFrom(_from common.Address, _to common.Address, _tokenId *big.Int, _data []byte) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.SafeTransferFrom(&_EthereumPaymentObligationContract.TransactOpts, _from, _to, _tokenId, _data) +// Solidity: function safeTransferFrom(from address, to address, tokenId uint256, _data bytes) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) SafeTransferFrom(from common.Address, to common.Address, tokenId *big.Int, _data []byte) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.SafeTransferFrom(&_EthereumPaymentObligationContract.TransactOpts, from, to, tokenId, _data) } // SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. // -// Solidity: function setApprovalForAll(_to address, _approved bool) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) SetApprovalForAll(opts *bind.TransactOpts, _to common.Address, _approved bool) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.contract.Transact(opts, "setApprovalForAll", _to, _approved) +// Solidity: function setApprovalForAll(to address, approved bool) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) SetApprovalForAll(opts *bind.TransactOpts, to common.Address, approved bool) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.contract.Transact(opts, "setApprovalForAll", to, approved) } // SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. // -// Solidity: function setApprovalForAll(_to address, _approved bool) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SetApprovalForAll(_to common.Address, _approved bool) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.SetApprovalForAll(&_EthereumPaymentObligationContract.TransactOpts, _to, _approved) +// Solidity: function setApprovalForAll(to address, approved bool) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) SetApprovalForAll(to common.Address, approved bool) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.SetApprovalForAll(&_EthereumPaymentObligationContract.TransactOpts, to, approved) } // SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. // -// Solidity: function setApprovalForAll(_to address, _approved bool) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) SetApprovalForAll(_to common.Address, _approved bool) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.SetApprovalForAll(&_EthereumPaymentObligationContract.TransactOpts, _to, _approved) +// Solidity: function setApprovalForAll(to address, approved bool) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) SetApprovalForAll(to common.Address, approved bool) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.SetApprovalForAll(&_EthereumPaymentObligationContract.TransactOpts, to, approved) } // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. // -// Solidity: function transferFrom(_from address, _to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) TransferFrom(opts *bind.TransactOpts, _from common.Address, _to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.contract.Transact(opts, "transferFrom", _from, _to, _tokenId) +// Solidity: function transferFrom(from address, to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactor) TransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.contract.Transact(opts, "transferFrom", from, to, tokenId) } // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. // -// Solidity: function transferFrom(_from address, _to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TransferFrom(_from common.Address, _to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.TransferFrom(&_EthereumPaymentObligationContract.TransactOpts, _from, _to, _tokenId) +// Solidity: function transferFrom(from address, to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractSession) TransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.TransferFrom(&_EthereumPaymentObligationContract.TransactOpts, from, to, tokenId) } // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. // -// Solidity: function transferFrom(_from address, _to address, _tokenId uint256) returns() -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) TransferFrom(_from common.Address, _to common.Address, _tokenId *big.Int) (*types.Transaction, error) { - return _EthereumPaymentObligationContract.Contract.TransferFrom(&_EthereumPaymentObligationContract.TransactOpts, _from, _to, _tokenId) +// Solidity: function transferFrom(from address, to address, tokenId uint256) returns() +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractTransactorSession) TransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _EthereumPaymentObligationContract.Contract.TransferFrom(&_EthereumPaymentObligationContract.TransactOpts, from, to, tokenId) } // EthereumPaymentObligationContractApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the EthereumPaymentObligationContract contract. @@ -754,23 +671,23 @@ type EthereumPaymentObligationContractApproval struct { // FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. // -// Solidity: e Approval(_owner indexed address, _approved indexed address, _tokenId indexed uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterApproval(opts *bind.FilterOpts, _owner []common.Address, _approved []common.Address, _tokenId []*big.Int) (*EthereumPaymentObligationContractApprovalIterator, error) { +// Solidity: e Approval(owner indexed address, approved indexed address, tokenId indexed uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterApproval(opts *bind.FilterOpts, owner []common.Address, approved []common.Address, tokenId []*big.Int) (*EthereumPaymentObligationContractApprovalIterator, error) { - var _ownerRule []interface{} - for _, _ownerItem := range _owner { - _ownerRule = append(_ownerRule, _ownerItem) + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) } - var _approvedRule []interface{} - for _, _approvedItem := range _approved { - _approvedRule = append(_approvedRule, _approvedItem) + var approvedRule []interface{} + for _, approvedItem := range approved { + approvedRule = append(approvedRule, approvedItem) } - var _tokenIdRule []interface{} - for _, _tokenIdItem := range _tokenId { - _tokenIdRule = append(_tokenIdRule, _tokenIdItem) + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "Approval", _ownerRule, _approvedRule, _tokenIdRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "Approval", ownerRule, approvedRule, tokenIdRule) if err != nil { return nil, err } @@ -779,23 +696,23 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilte // WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. // -// Solidity: e Approval(_owner indexed address, _approved indexed address, _tokenId indexed uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractApproval, _owner []common.Address, _approved []common.Address, _tokenId []*big.Int) (event.Subscription, error) { +// Solidity: e Approval(owner indexed address, approved indexed address, tokenId indexed uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractApproval, owner []common.Address, approved []common.Address, tokenId []*big.Int) (event.Subscription, error) { - var _ownerRule []interface{} - for _, _ownerItem := range _owner { - _ownerRule = append(_ownerRule, _ownerItem) + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) } - var _approvedRule []interface{} - for _, _approvedItem := range _approved { - _approvedRule = append(_approvedRule, _approvedItem) + var approvedRule []interface{} + for _, approvedItem := range approved { + approvedRule = append(approvedRule, approvedItem) } - var _tokenIdRule []interface{} - for _, _tokenIdItem := range _tokenId { - _tokenIdRule = append(_tokenIdRule, _tokenIdItem) + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "Approval", _ownerRule, _approvedRule, _tokenIdRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "Approval", ownerRule, approvedRule, tokenIdRule) if err != nil { return nil, err } @@ -904,19 +821,19 @@ type EthereumPaymentObligationContractApprovalForAll struct { // FilterApprovalForAll is a free log retrieval operation binding the contract event 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31. // -// Solidity: e ApprovalForAll(_owner indexed address, _operator indexed address, _approved bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterApprovalForAll(opts *bind.FilterOpts, _owner []common.Address, _operator []common.Address) (*EthereumPaymentObligationContractApprovalForAllIterator, error) { +// Solidity: e ApprovalForAll(owner indexed address, operator indexed address, approved bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterApprovalForAll(opts *bind.FilterOpts, owner []common.Address, operator []common.Address) (*EthereumPaymentObligationContractApprovalForAllIterator, error) { - var _ownerRule []interface{} - for _, _ownerItem := range _owner { - _ownerRule = append(_ownerRule, _ownerItem) + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) } - var _operatorRule []interface{} - for _, _operatorItem := range _operator { - _operatorRule = append(_operatorRule, _operatorItem) + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "ApprovalForAll", _ownerRule, _operatorRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "ApprovalForAll", ownerRule, operatorRule) if err != nil { return nil, err } @@ -925,19 +842,19 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilte // WatchApprovalForAll is a free log subscription operation binding the contract event 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31. // -// Solidity: e ApprovalForAll(_owner indexed address, _operator indexed address, _approved bool) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchApprovalForAll(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractApprovalForAll, _owner []common.Address, _operator []common.Address) (event.Subscription, error) { +// Solidity: e ApprovalForAll(owner indexed address, operator indexed address, approved bool) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchApprovalForAll(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractApprovalForAll, owner []common.Address, operator []common.Address) (event.Subscription, error) { - var _ownerRule []interface{} - for _, _ownerItem := range _owner { - _ownerRule = append(_ownerRule, _ownerItem) + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) } - var _operatorRule []interface{} - for _, _operatorItem := range _operator { - _operatorRule = append(_operatorRule, _operatorItem) + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "ApprovalForAll", _ownerRule, _operatorRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "ApprovalForAll", ownerRule, operatorRule) if err != nil { return nil, err } @@ -1170,23 +1087,23 @@ type EthereumPaymentObligationContractTransfer struct { // FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. // -// Solidity: e Transfer(_from indexed address, _to indexed address, _tokenId indexed uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterTransfer(opts *bind.FilterOpts, _from []common.Address, _to []common.Address, _tokenId []*big.Int) (*EthereumPaymentObligationContractTransferIterator, error) { +// Solidity: e Transfer(from indexed address, to indexed address, tokenId indexed uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) FilterTransfer(opts *bind.FilterOpts, from []common.Address, to []common.Address, tokenId []*big.Int) (*EthereumPaymentObligationContractTransferIterator, error) { - var _fromRule []interface{} - for _, _fromItem := range _from { - _fromRule = append(_fromRule, _fromItem) + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) } - var _toRule []interface{} - for _, _toItem := range _to { - _toRule = append(_toRule, _toItem) + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) } - var _tokenIdRule []interface{} - for _, _tokenIdItem := range _tokenId { - _tokenIdRule = append(_tokenIdRule, _tokenIdItem) + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "Transfer", _fromRule, _toRule, _tokenIdRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.FilterLogs(opts, "Transfer", fromRule, toRule, tokenIdRule) if err != nil { return nil, err } @@ -1195,23 +1112,23 @@ func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilte // WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. // -// Solidity: e Transfer(_from indexed address, _to indexed address, _tokenId indexed uint256) -func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractTransfer, _from []common.Address, _to []common.Address, _tokenId []*big.Int) (event.Subscription, error) { +// Solidity: e Transfer(from indexed address, to indexed address, tokenId indexed uint256) +func (_EthereumPaymentObligationContract *EthereumPaymentObligationContractFilterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *EthereumPaymentObligationContractTransfer, from []common.Address, to []common.Address, tokenId []*big.Int) (event.Subscription, error) { - var _fromRule []interface{} - for _, _fromItem := range _from { - _fromRule = append(_fromRule, _fromItem) + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) } - var _toRule []interface{} - for _, _toItem := range _to { - _toRule = append(_toRule, _toItem) + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) } - var _tokenIdRule []interface{} - for _, _tokenIdItem := range _tokenId { - _tokenIdRule = append(_tokenIdRule, _tokenIdItem) + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) } - logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "Transfer", _fromRule, _toRule, _tokenIdRule) + logs, sub, err := _EthereumPaymentObligationContract.contract.WatchLogs(opts, "Transfer", fromRule, toRule, tokenIdRule) if err != nil { return nil, err } diff --git a/nft/ethereum_payment_obligation_test.go b/nft/ethereum_payment_obligation_test.go index 991a59547..b194fa56f 100644 --- a/nft/ethereum_payment_obligation_test.go +++ b/nft/ethereum_payment_obligation_test.go @@ -3,19 +3,23 @@ package nft import ( - "errors" "math/big" "testing" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/documents/invoice" - "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/ethereum" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/nft" + "github.com/centrifuge/go-centrifuge/queue" "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/utils" + "github.com/centrifuge/precise-proofs/proofs" "github.com/centrifuge/precise-proofs/proofs/proto" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -38,22 +42,22 @@ func TestCreateProofData(t *testing.T) { "happypath", []*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "value1", Salt: salt, SortedHashes: sortedHashes, }, { - Property: "prop2", + Property: proofs.ReadableName("prop2"), Value: "value2", Salt: salt, SortedHashes: sortedHashes, }, }, proofData{ - Values: [3]string{"value1", "value2"}, - Proofs: [3][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, - Salts: [3][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, + Values: []string{"value1", "value2"}, + Proofs: [][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, + Salts: [][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, }, nil, }, @@ -61,22 +65,22 @@ func TestCreateProofData(t *testing.T) { "invalid hashes", []*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "value1", Salt: salt, SortedHashes: [][]byte{utils.RandomSlice(33), utils.RandomSlice(31)}, }, { - Property: "prop2", + Property: proofs.ReadableName("prop2"), Value: "value2", Salt: salt, SortedHashes: [][]byte{utils.RandomSlice(33), utils.RandomSlice(31)}, }, }, proofData{ - Values: [3]string{"value1", "value2"}, - Proofs: [3][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, - Salts: [3][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, + Values: []string{"value1", "value2"}, + Proofs: [][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, + Salts: [][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, }, errors.New("input exceeds length of 32"), }, @@ -84,22 +88,22 @@ func TestCreateProofData(t *testing.T) { "invalid salts", []*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "value1", Salt: utils.RandomSlice(33), SortedHashes: sortedHashes, }, { - Property: "prop2", + Property: proofs.ReadableName("prop2"), Value: "value2", Salt: utils.RandomSlice(32), SortedHashes: sortedHashes, }, }, proofData{ - Values: [3]string{"value1", "value2"}, - Proofs: [3][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, - Salts: [3][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, + Values: []string{"value1", "value2"}, + Proofs: [][][32]byte{{byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}, {byteSliceToByteArray32(sortedHashes[0]), byteSliceToByteArray32(sortedHashes[1])}}, + Salts: [][32]byte{byteSliceToByteArray32(salt), byteSliceToByteArray32(salt)}, }, errors.New("input exceeds length of 32"), }, @@ -124,73 +128,63 @@ type MockPaymentObligation struct { mock.Mock } -func (m *MockPaymentObligation) Mint(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values [3]string, _salts [3][32]byte, _proofs [3][][32]byte) (*types.Transaction, error) { +func (m *MockPaymentObligation) Mint(opts *bind.TransactOpts, _to common.Address, _tokenId *big.Int, _tokenURI string, _anchorId *big.Int, _merkleRoot [32]byte, _values []string, _salts [][32]byte, _proofs [][][32]byte) (*types.Transaction, error) { args := m.Called(opts, _to, _tokenId, _tokenURI, _anchorId, _merkleRoot, _values, _salts, _proofs) return args.Get(0).(*types.Transaction), args.Error(1) } -type MockConfig struct { - mock.Mock -} - -func (m *MockConfig) GetIdentityID() ([]byte, error) { - args := m.Called() - return args.Get(0).([]byte), args.Error(1) -} - -func (m *MockConfig) GetEthereumDefaultAccountName() string { - args := m.Called() - return args.Get(0).(string) -} - func TestPaymentObligationService(t *testing.T) { tests := []struct { name string - mocker func() (testingdocuments.MockService, *MockPaymentObligation, testingcommons.MockIDService, testingcommons.MockEthClient, MockConfig) + mocker func() (testingdocuments.MockService, *MockPaymentObligation, testingcommons.MockIDService, testingcommons.MockEthClient, testingconfig.MockConfig) request *nftpb.NFTMintRequest err error result string }{ { "happypath", - func() (testingdocuments.MockService, *MockPaymentObligation, testingcommons.MockIDService, testingcommons.MockEthClient, MockConfig) { - centIDByte := utils.RandomSlice(6) - centID, _ := identity.ToCentID(centIDByte) - address := common.BytesToAddress(utils.RandomSlice(32)) + func() (testingdocuments.MockService, *MockPaymentObligation, testingcommons.MockIDService, testingcommons.MockEthClient, testingconfig.MockConfig) { coreDoc := coredocument.New() coreDoc.DocumentRoot = utils.RandomSlice(32) proof := getDummyProof(coreDoc) docServiceMock := testingdocuments.MockService{} docServiceMock.On("GetCurrentVersion", decodeHex("0x1212")).Return(&invoice.Invoice{InvoiceNumber: "1232", CoreDocument: coreDoc}, nil) - docServiceMock.On("CreateProofs", decodeHex("0x1212"), []string{"somefield"}).Return(proof, nil) + docServiceMock.On("CreateProofs", decodeHex("0x1212"), []string{"collaborators[0]"}).Return(proof, nil) + docServiceMock.On("Exists").Return(true) paymentObligationMock := &MockPaymentObligation{} idServiceMock := testingcommons.MockIDService{} - idServiceMock.On("GetIdentityAddress", centID).Return(address, nil) ethClientMock := testingcommons.MockEthClient{} ethClientMock.On("GetTxOpts", "ethacc").Return(&bind.TransactOpts{}, nil) ethClientMock.On("SubmitTransactionWithRetries", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, ).Return(&types.Transaction{}, nil) - configMock := MockConfig{} + configMock := testingconfig.MockConfig{} configMock.On("GetEthereumDefaultAccountName").Return("ethacc") - configMock.On("GetIdentityID").Return(centIDByte, nil) + return docServiceMock, paymentObligationMock, idServiceMock, ethClientMock, configMock }, - &nftpb.NFTMintRequest{Identifier: "0x1212", Type: "happypath", ProofFields: []string{"somefield"}}, + &nftpb.NFTMintRequest{Identifier: "0x1212", ProofFields: []string{"collaborators[0]"}, DepositAddress: "0xf72855759a39fb75fc7341139f5d7a3974d4da08"}, nil, "", }, } + + registry := documents.NewServiceRegistry() for _, test := range tests { t.Run(test.name, func(t *testing.T) { // get mocks - docService, paymentOb, idService, ethClient, config := test.mocker() + docService, paymentOb, idService, ethClient, mockCfg := test.mocker() // with below config the documentType has to be test.name to avoid conflicts since registry is a singleton - documents.GetRegistryInstance().Register(test.name, &docService) - service := NewEthereumPaymentObligation(paymentOb, &idService, ðClient, &config) - tokenID, err := service.MintNFT(decodeHex(test.request.Identifier), test.request.Type, test.request.RegistryAddress, test.request.DepositAddress, test.request.ProofFields) + registry.Register(test.name, &docService) + confirmations := make(chan *watchTokenMinted) + service := newEthereumPaymentObligation(registry, &idService, ðClient, &mockCfg, nil, func(config Config, qs *queue.Server, tokenID *big.Int, registryAddress string) (chan *watchTokenMinted, error) { + return confirmations, nil + }, func(address common.Address, client ethereum.Client) (*EthereumPaymentObligationContract, error) { + return &EthereumPaymentObligationContract{}, nil + }) + _, err := service.MintNFT(decodeHex(test.request.Identifier), test.request.RegistryAddress, test.request.DepositAddress, test.request.ProofFields) if test.err != nil { assert.Equal(t, test.err.Error(), err.Error()) } else if err != nil { @@ -200,20 +194,19 @@ func TestPaymentObligationService(t *testing.T) { paymentOb.AssertExpectations(t) idService.AssertExpectations(t) ethClient.AssertExpectations(t) - config.AssertExpectations(t) - assert.NotEmpty(t, tokenID) + mockCfg.AssertExpectations(t) }) } } func getDummyProof(coreDoc *coredocumentpb.CoreDocument) *documents.DocumentProof { return &documents.DocumentProof{ - DocumentId: coreDoc.DocumentIdentifier, - VersionId: coreDoc.CurrentVersion, + DocumentID: coreDoc.DocumentIdentifier, + VersionID: coreDoc.CurrentVersion, State: "state", FieldProofs: []*proofspb.Proof{ { - Property: "prop1", + Property: proofs.ReadableName("prop1"), Value: "val1", Salt: utils.RandomSlice(32), Hash: utils.RandomSlice(32), @@ -224,7 +217,7 @@ func getDummyProof(coreDoc *coredocumentpb.CoreDocument) *documents.DocumentProo }, }, { - Property: "prop2", + Property: proofs.ReadableName("prop2"), Value: "val2", Salt: utils.RandomSlice(32), Hash: utils.RandomSlice(32), diff --git a/nft/handler.go b/nft/handler.go index 48c28891b..a68f62b72 100644 --- a/nft/handler.go +++ b/nft/handler.go @@ -3,6 +3,8 @@ package nft import ( "context" + "github.com/ethereum/go-ethereum/common" + "github.com/centrifuge/go-centrifuge/centerrors" "github.com/centrifuge/go-centrifuge/code" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/nft" @@ -17,21 +19,41 @@ type grpcHandler struct { } // GRPCHandler returns an implementation of invoice.DocumentServiceServer -func GRPCHandler() nftpb.NFTServiceServer { - return &grpcHandler{service: GetPaymentObligation()} +func GRPCHandler(payOb PaymentObligation) nftpb.NFTServiceServer { + return &grpcHandler{service: payOb} } // MintNFT will be called from the client API to mint an NFT func (g grpcHandler) MintNFT(context context.Context, request *nftpb.NFTMintRequest) (*nftpb.NFTMintResponse, error) { - apiLog.Infof("Received request to Mint an NFT for document %s type %s with proof fields %s", request.Identifier, request.Type, request.ProofFields) + apiLog.Infof("Received request to Mint an NFT with %s with proof fields %s", request.Identifier, request.ProofFields) + + err := validateParameters(request) + if err != nil { + return nil, err + } identifier, err := hexutil.Decode(request.Identifier) if err != nil { return &nftpb.NFTMintResponse{}, centerrors.New(code.Unknown, err.Error()) } - tokenID, err := g.service.MintNFT(identifier, request.Type, request.RegistryAddress, request.DepositAddress, request.ProofFields) + confirmation, err := g.service.MintNFT(identifier, request.RegistryAddress, request.DepositAddress, request.ProofFields) if err != nil { return &nftpb.NFTMintResponse{}, centerrors.New(code.Unknown, err.Error()) } - return &nftpb.NFTMintResponse{TokenId: tokenID}, nil + watchToken := <-confirmation + return &nftpb.NFTMintResponse{TokenId: watchToken.TokenID.String()}, watchToken.Err +} + +func validateParameters(request *nftpb.NFTMintRequest) error { + + if !common.IsHexAddress(request.RegistryAddress) { + return centerrors.New(code.Unknown, "RegistryAddress is not a valid Ethereum address") + } + + if !common.IsHexAddress(request.DepositAddress) { + return centerrors.New(code.Unknown, "DepositAddress is not a valid Ethereum address") + } + + return nil + } diff --git a/nft/handler_test.go b/nft/handler_test.go index b389ff60f..1f59b3249 100644 --- a/nft/handler_test.go +++ b/nft/handler_test.go @@ -6,9 +6,11 @@ import ( "context" "testing" + "math/big" + + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/nft" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/go-errors/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -17,25 +19,32 @@ type MockPaymentObligationService struct { mock.Mock } -func (m *MockPaymentObligationService) MintNFT(documentID []byte, docType, registryAddress, depositAddress string, proofFields []string) (string, error) { - args := m.Called(documentID, docType, registryAddress, depositAddress, proofFields) - return args.Get(0).(string), args.Error(1) +func (m *MockPaymentObligationService) MintNFT(documentID []byte, registryAddress, depositAddress string, proofFields []string) (<-chan *watchTokenMinted, error) { + args := m.Called(documentID, registryAddress, depositAddress, proofFields) + return args.Get(0).(chan *watchTokenMinted), args.Error(1) } func TestNFTMint_success(t *testing.T) { nftMintRequest := getTestSetupData() mockService := &MockPaymentObligationService{} docID, _ := hexutil.Decode(nftMintRequest.Identifier) + + confirmations := make(chan *watchTokenMinted) mockService. - On("MintNFT", docID, nftMintRequest.Type, nftMintRequest.RegistryAddress, nftMintRequest.DepositAddress, nftMintRequest.ProofFields). - Return("tokenID", nil) + On("MintNFT", docID, nftMintRequest.RegistryAddress, nftMintRequest.DepositAddress, nftMintRequest.ProofFields). + Return(confirmations, nil) + + tokID := big.NewInt(1) + go func() { + confirmations <- &watchTokenMinted{tokID, nil} + }() handler := grpcHandler{mockService} nftMintResponse, err := handler.MintNFT(context.Background(), nftMintRequest) mockService.AssertExpectations(t) assert.Nil(t, err, "mint nft should be successful") - assert.NotEqual(t, "", nftMintResponse.TokenId, "tokenId should have a dummy value") + assert.Equal(t, tokID.String(), nftMintResponse.TokenId, "TokenID should have a dummy value") } func TestNFTMint_InvalidIdentifier(t *testing.T) { @@ -50,9 +59,10 @@ func TestNFTMint_ServiceError(t *testing.T) { nftMintRequest := getTestSetupData() mockService := &MockPaymentObligationService{} docID, _ := hexutil.Decode(nftMintRequest.Identifier) + confirmations := make(chan *watchTokenMinted) mockService. - On("MintNFT", docID, nftMintRequest.Type, nftMintRequest.RegistryAddress, nftMintRequest.DepositAddress, nftMintRequest.ProofFields). - Return("", errors.New("service error")) + On("MintNFT", docID, nftMintRequest.RegistryAddress, nftMintRequest.DepositAddress, nftMintRequest.ProofFields). + Return(confirmations, errors.New("service error")) handler := grpcHandler{mockService} _, err := handler.MintNFT(context.Background(), nftMintRequest) @@ -60,6 +70,20 @@ func TestNFTMint_ServiceError(t *testing.T) { assert.NotNil(t, err) } +func TestNFTMint_InvalidAddresses(t *testing.T) { + nftMintRequest := getTestSetupData() + nftMintRequest.RegistryAddress = "0x1234" + handler := grpcHandler{&MockPaymentObligationService{}} + _, err := handler.MintNFT(context.Background(), nftMintRequest) + assert.Error(t, err, "invalid registry address should throw an error") + + nftMintRequest = getTestSetupData() + nftMintRequest.DepositAddress = "abc" + handler = grpcHandler{&MockPaymentObligationService{}} + _, err = handler.MintNFT(context.Background(), nftMintRequest) + assert.Error(t, err, "invalid deposit address should throw an error") +} + func getTestSetupData() *nftpb.NFTMintRequest { return &nftpb.NFTMintRequest{ Identifier: "0x12121212", diff --git a/nft/minting_confirmation_task.go b/nft/minting_confirmation_task.go new file mode 100644 index 000000000..2469ccb8e --- /dev/null +++ b/nft/minting_confirmation_task.go @@ -0,0 +1,147 @@ +package nft + +import ( + "context" + "time" + + "github.com/centrifuge/go-centrifuge/centerrors" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/centrifuge/gocelery" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +const ( + mintingConfirmationTaskName string = "MintingConfirmationTaskName" + tokenIDParam string = "TokenIDParam" + registryAddressParam string = "RegistryAddressParam" +) + +// paymentObligationMintedFilterer filters the approved NFTs +type paymentObligationMintedFilterer interface { + + // FilterPaymentObligationMinted filters PaymentObligationMinted events + FilterPaymentObligationMinted(opts *bind.FilterOpts) (*EthereumPaymentObligationContractPaymentObligationMintedIterator, error) +} + +// mintingConfirmationTask confirms the minting of a payment obligation NFT +type mintingConfirmationTask struct { + //task parameter + TokenID string + BlockHeight uint64 + RegistryAddress string + Timeout time.Duration + + //state + EthContextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc) +} + +func newMintingConfirmationTask( + timeout time.Duration, + ethContextInitializer func(d time.Duration) (ctx context.Context, cancelFunc context.CancelFunc), +) *mintingConfirmationTask { + return &mintingConfirmationTask{ + Timeout: timeout, + EthContextInitializer: ethContextInitializer, + } +} + +// TaskTypeName returns mintingConfirmationTaskName +func (nftc *mintingConfirmationTask) TaskTypeName() string { + return mintingConfirmationTaskName +} + +// Copy returns a new instance of mintingConfirmationTask +func (nftc *mintingConfirmationTask) Copy() (gocelery.CeleryTask, error) { + return &mintingConfirmationTask{ + nftc.TokenID, + nftc.BlockHeight, + nftc.RegistryAddress, + nftc.Timeout, + nftc.EthContextInitializer, + }, nil +} + +// ParseKwargs - define a method to parse CentID +func (nftc *mintingConfirmationTask) ParseKwargs(kwargs map[string]interface{}) (err error) { + // parse TokenID + tokenID, ok := kwargs[tokenIDParam] + if !ok { + return errors.New("undefined kwarg " + tokenIDParam) + } + nftc.TokenID, ok = tokenID.(string) + if !ok { + return errors.New("malformed kwarg [%s]", tokenIDParam) + } + + // parse BlockHeight + nftc.BlockHeight, err = queue.ParseBlockHeight(kwargs) + if err != nil { + return err + } + + //parse RegistryAddress + registryAddress, ok := kwargs[registryAddressParam] + if !ok { + return errors.New("undefined kwarg " + registryAddressParam) + } + + nftc.RegistryAddress, ok = registryAddress.(string) + if !ok { + return errors.New("malformed kwarg [%s]", registryAddressParam) + } + + // override TimeoutParam if provided + tdRaw, ok := kwargs[queue.TimeoutParam] + if ok { + td, err := queue.GetDuration(tdRaw) + if err != nil { + return errors.New("malformed kwarg [%s] because [%s]", queue.TimeoutParam, err.Error()) + } + nftc.Timeout = td + } + + return nil +} + +// RunTask calls listens to events from geth related to MintingConfirmationTask#TokenID and records result. +func (nftc *mintingConfirmationTask) RunTask() (interface{}, error) { + log.Infof("Waiting for confirmation for the minting of token [%x]", nftc.TokenID) + + ethContext, _ := nftc.EthContextInitializer(nftc.Timeout) + + fOpts := &bind.FilterOpts{ + Context: ethContext, + Start: nftc.BlockHeight, + } + + var filter paymentObligationMintedFilterer + var err error + + filter, err = bindContract(common.HexToAddress(nftc.RegistryAddress), ethereum.GetClient()) + + if err != nil { + return nil, err + } + + for { + iter, err := filter.FilterPaymentObligationMinted(fOpts) + if err != nil { + return nil, centerrors.Wrap(err, "failed to start filtering token minted logs") + } + + err = utils.LookForEvent(iter) + if err == nil { + log.Infof("Received filtered event NFT minted for token [%s] \n", nftc.TokenID) + return iter.Event, nil + } + + if err != utils.ErrEventNotFound { + return nil, err + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/nft/minting_confirmation_task_test.go b/nft/minting_confirmation_task_test.go new file mode 100644 index 000000000..075bdf828 --- /dev/null +++ b/nft/minting_confirmation_task_test.go @@ -0,0 +1,67 @@ +// +build unit + +package nft + +import ( + "encoding/hex" + "testing" + + "github.com/centrifuge/go-centrifuge/queue" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/stretchr/testify/assert" +) + +func TestMintingConfirmationTask_ParseKwargs_success(t *testing.T) { + task := mintingConfirmationTask{} + tokenId := hex.EncodeToString(utils.RandomSlice(256)) + blockHeight := uint64(12) + registryAddress := "0xf72855759a39fb75fc7341139f5d7a3974d4da08" + + kwargs := map[string]interface{}{ + tokenIDParam: tokenId, + queue.BlockHeightParam: blockHeight, + registryAddressParam: registryAddress, + } + + 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, tokenId, task.TokenID, "tokenId should be parsed correctly") + assert.Equal(t, blockHeight, task.BlockHeight, "blockHeight should be parsed correctly") + assert.Equal(t, registryAddress, task.RegistryAddress, "registryAddress should be parsed correctly") + +} + +func TestMintingConfirmationTask_ParseKwargs_fail(t *testing.T) { + task := mintingConfirmationTask{} + tests := []map[string]interface{}{ + { + queue.BlockHeightParam: uint64(12), + registryAddressParam: "0xf72855759a39fb75fc7341139f5d7a3974d4da08", + }, + { + tokenIDParam: hex.EncodeToString(utils.RandomSlice(256)), + registryAddressParam: "0xf72855759a39fb75fc7341139f5d7a3974d4da08", + }, + { + tokenIDParam: hex.EncodeToString(utils.RandomSlice(256)), + queue.BlockHeightParam: uint64(12), + }, + { + //empty map + + }, + { + "dummy": "dummy", + }, + } + + for i, test := range tests { + decoded, err := utils.SimulateJSONDecodeForGocelery(test) + assert.Nil(t, err, "json decode should not thrown an error") + err = task.ParseKwargs(decoded) + assert.Error(t, err, "test case %v: parsing should fail", i) + } +} diff --git a/nft/payment_obligation.go b/nft/payment_obligation.go index 1000a354c..b833a7fd1 100644 --- a/nft/payment_obligation.go +++ b/nft/payment_obligation.go @@ -1,8 +1,15 @@ package nft +import "math/big" + // PaymentObligation handles transactions related to minting of NFTs type PaymentObligation interface { // MintNFT mints an NFT - MintNFT(documentID []byte, docType, registryAddress, depositAddress string, proofFields []string) (string, error) + MintNFT(documentID []byte, registryAddress, depositAddress string, proofFields []string) (<-chan *watchTokenMinted, error) +} + +type watchTokenMinted struct { + TokenID *big.Int + Err error } diff --git a/nft/payment_obligation_integration_test.go b/nft/payment_obligation_integration_test.go index 3889721de..1ec5cca2c 100644 --- a/nft/payment_obligation_integration_test.go +++ b/nft/payment_obligation_integration_test.go @@ -9,44 +9,60 @@ import ( "time" "github.com/centrifuge/centrifuge-protobufs/documenttypes" - "github.com/centrifuge/go-centrifuge/config" - cc "github.com/centrifuge/go-centrifuge/context/testingbootstrap" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/documents/invoice" - "github.com/centrifuge/go-centrifuge/nft" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/protobufs/gen/go/invoice" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/golang/protobuf/ptypes/timestamp" "github.com/stretchr/testify/assert" + + "github.com/centrifuge/go-centrifuge/config" + cc "github.com/centrifuge/go-centrifuge/context/testingbootstrap" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/nft" + "github.com/ethereum/go-ethereum/log" ) +var registry *documents.ServiceRegistry +var cfg config.Configuration +var idService identity.Service +var payOb nft.PaymentObligation + func TestMain(m *testing.M) { - cc.TestFunctionalEthereumBootstrap() - prevSignPubkey := config.Config.V.Get("keys.signing.publicKey") - prevSignPrivkey := config.Config.V.Get("keys.signing.privateKey") - prevEthPubkey := config.Config.V.Get("keys.ethauth.publicKey") - prevEthPrivkey := config.Config.V.Get("keys.ethauth.privateKey") - config.Config.V.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") - config.Config.V.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") - config.Config.V.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") - config.Config.V.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + log.Debug("Test PreSetup for NFT") + ctx := cc.TestFunctionalEthereumBootstrap() + registry = ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + idService = ctx[identity.BootstrappedIDService].(identity.Service) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + payOb = ctx[nft.BootstrappedPayObService].(nft.PaymentObligation) + prevSignPubkey := cfg.Get("keys.signing.publicKey") + prevSignPrivkey := cfg.Get("keys.signing.privateKey") + prevEthPubkey := cfg.Get("keys.ethauth.publicKey") + prevEthPrivkey := cfg.Get("keys.ethauth.privateKey") + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") result := m.Run() - config.Config.V.Set("keys.signing.publicKey", prevSignPubkey) - config.Config.V.Set("keys.signing.privateKey", prevSignPrivkey) - config.Config.V.Set("keys.ethauth.publicKey", prevEthPubkey) - config.Config.V.Set("keys.ethauth.privateKey", prevEthPrivkey) + cfg.Set("keys.signing.publicKey", prevSignPubkey) + cfg.Set("keys.signing.privateKey", prevSignPrivkey) + cfg.Set("keys.ethauth.publicKey", prevEthPubkey) + cfg.Set("keys.ethauth.privateKey", prevEthPrivkey) cc.TestFunctionalEthereumTearDown() os.Exit(result) } func TestPaymentObligationService_mint(t *testing.T) { // create identity - testingutils.CreateIdentityWithKeys() + log.Debug("Create Identity for Testing") + testingidentity.CreateIdentityWithKeys(cfg, idService) // create invoice (anchor) - service, err := documents.GetRegistryInstance().LocateService(documenttypes.InvoiceDataTypeUrl) + service, err := registry.LocateService(documenttypes.InvoiceDataTypeUrl) assert.Nil(t, err, "should not error out when getting invoice service") - contextHeader, err := documents.NewContextHeader() + contextHeader, err := header.NewContextHeader(context.Background(), cfg) assert.Nil(t, err) invoiceService := service.(invoice.Service) dueDate := time.Now().Add(4 * 24 * time.Hour) @@ -62,19 +78,21 @@ func TestPaymentObligationService_mint(t *testing.T) { }, }, contextHeader) assert.Nil(t, err, "should not error out when creating invoice model") - modelUpdated, err := invoiceService.Create(context.Background(), model) + modelUpdated, err := invoiceService.Create(contextHeader, model) // get ID ID, err := modelUpdated.ID() assert.Nil(t, err, "should not error out when getting invoice ID") // call mint // assert no error - _, err = nft.GetPaymentObligation().MintNFT( + confirmations, err := payOb.MintNFT( ID, - documenttypes.InvoiceDataTypeUrl, - "doesntmatter", - "doesntmatter", - []string{"gross_amount", "currency", "due_date"}, + cfg.GetContractAddress("paymentObligation").String(), + "0xf72855759a39fb75fc7341139f5d7a3974d4da08", + []string{"invoice.gross_amount", "invoice.currency", "invoice.due_date", "collaborators[0]"}, ) assert.Nil(t, err, "should not error out when minting an invoice") + tokenConfirm := <-confirmations + assert.Nil(t, tokenConfirm.Err, "should not error out when minting an invoice") + assert.NotNil(t, tokenConfirm.TokenID, "token id should be present") } diff --git a/node/bootstrapper.go b/node/bootstrapper.go index 432b70aad..719890db7 100644 --- a/node/bootstrapper.go +++ b/node/bootstrapper.go @@ -2,67 +2,72 @@ package node import ( "context" - "errors" - "fmt" "os" "os/signal" - "github.com/centrifuge/go-centrifuge/api" "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" - "github.com/centrifuge/go-centrifuge/p2p" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/storage" + "github.com/syndtr/goleveldb/leveldb" ) -type Bootstrapper struct { -} +// Bootstrapper implements bootstrap.Bootstrapper. +type Bootstrapper struct{} +// Bootstrap runs the servers. +// Note: this is a blocking call. func (*Bootstrapper) Bootstrap(c map[string]interface{}) error { - if _, ok := c[bootstrap.BootstrappedConfig]; ok { - services, err := defaultServerList() - if err != nil { - return fmt.Errorf("failed to get default server list: %v", err) - } + srvs, err := GetServers(c) + if err != nil { + cleanUp(c) + return errors.New("failed to load servers: %v", err) + } - n := NewNode(services) - feedback := make(chan error) - // may be we can pass a context that exists in c here - ctx, canc := context.WithCancel(context.Background()) - go n.Start(ctx, feedback) - controlC := make(chan os.Signal, 1) - signal.Notify(controlC, os.Interrupt) - for { - select { - case err := <-feedback: - panic(err) - case sig := <-controlC: - log.Info("Node shutting down because of ", sig) - canc() - err := <-feedback - return err - } - } - return nil + n := New(srvs) + feedback := make(chan error) + // may be we can pass a context that exists in c here + ctx, canc := context.WithCancel(context.WithValue(context.Background(), bootstrap.NodeObjRegistry, c)) + go n.Start(ctx, feedback) + controlC := make(chan os.Signal, 1) + signal.Notify(controlC, os.Interrupt) + select { + case err := <-feedback: + cleanUp(c) + panic(err) + case sig := <-controlC: + log.Info("Node shutting down because of ", sig) + canc() + cleanUp(c) + err := <-feedback + return err } - return errors.New("could not initialize node") + } -func defaultServerList() ([]Server, error) { - publicKey, privateKey, err := ed25519.GetSigningKeyPairFromConfig() - if err != nil { - return nil, fmt.Errorf("failed to get keys: %v", err) +func cleanUp(c map[string]interface{}) { + // close the node db + db := c[storage.BootstrappedLevelDB].(*leveldb.DB) + db.Close() +} + +// GetServers gets the long running background services in the node as a list +func GetServers(ctx map[string]interface{}) ([]Server, error) { + p2pSrv, ok := ctx[bootstrap.BootstrappedP2PServer] + if !ok { + return nil, errors.New("p2p server not initialized") + } + + apiSrv, ok := ctx[bootstrap.BootstrappedAPIServer] + if !ok { + return nil, errors.New("API server not initialized") + } + + queueSrv, ok := ctx[bootstrap.BootstrappedQueueServer] + if !ok { + return nil, errors.New("queue server not initialized") } - return []Server{ - api.NewCentAPIServer( - config.Config.GetServerAddress(), - config.Config.GetServerPort(), - config.Config.GetNetworkString(), - ), - p2p.NewCentP2PServer( - config.Config.GetP2PPort(), - config.Config.GetBootstrapPeers(), - publicKey, privateKey, - ), - }, nil + var servers []Server + servers = append(servers, p2pSrv.(Server), apiSrv.(Server), queueSrv.(Server)) + return servers, nil } diff --git a/node/node.go b/node/node.go index 0699e3bea..00fdcdeb2 100644 --- a/node/node.go +++ b/node/node.go @@ -1,4 +1,4 @@ -// node provides utilities to control all long running background services on Centrifuge node +// Package node provides utilities to control all long running background services on Centrifuge node package node import ( @@ -17,7 +17,7 @@ type Server interface { Name() string // Start starts the service, expectation is that this would always be called in a separate go routine. - // WaitGroup contract should always be honoured by calling `defer wg.Done()` + // WaitGroup contract must always be honoured by calling `defer wg.Done()` Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) } @@ -26,12 +26,14 @@ type Node struct { services []Server } -func NewNode(services []Server) *Node { +// New returns a new Node with given services. +func New(services []Server) *Node { return &Node{ services: services, } } +// Name returns "CentNode". func (n *Node) Name() string { return "CentNode" } @@ -39,30 +41,31 @@ func (n *Node) Name() string { // Start starts all services that are wired in the Node and waits for further actions depending on errors or commands from upstream. func (n *Node) Start(ctx context.Context, startupErr chan<- error) { ctxCh, cancel := context.WithCancel(ctx) + defer cancel() childErr := make(chan error) var wg sync.WaitGroup wg.Add(len(n.services)) for _, s := range n.services { go s.Start(ctxCh, &wg, childErr) } - for { - select { - case errOut := <-childErr: - log.Error("Node received error from child service, stopping all child services", errOut) - // if one of the children fails to start all should stop - cancel() - // send the error upstream - startupErr <- errOut - wg.Wait() - return - case <-ctx.Done(): - log.Info("Node received context.done signal, stopping all child services") - // Note that in this case the children will also receive the done signal via the passed on context - wg.Wait() - log.Info("Node stopped all child services") - // special case to make the caller wait until servers are shutdown - startupErr <- nil - return - } + + select { + case errOut := <-childErr: + log.Error("Node received error from child service, stopping all child services", errOut) + // if one of the children fails to start all should stop + cancel() + // send the error upstream + startupErr <- errOut + wg.Wait() + return + case <-ctx.Done(): + log.Info("Node received context.done signal, stopping all child services") + // Note that in this case the children will also receive the done signal via the passed on context + wg.Wait() + log.Info("Node stopped all child services") + // special case to make the caller wait until servers are shutdown + startupErr <- nil + return } + } diff --git a/node/node_test.go b/node/node_test.go index 403b5b671..3d973b7d0 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -4,11 +4,12 @@ package node import ( "context" - "errors" "sync" "testing" "time" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/stretchr/testify/assert" ) @@ -51,7 +52,7 @@ func (s *MockService) Start(ctx context.Context, wg *sync.WaitGroup, startupErr func TestNode_StartHappy(t *testing.T) { // create node with two mocked out services services := []Server{&MockService{mustReturnStartErr: false}, &MockService{mustReturnStartErr: false}} - n := NewNode(services) + n := New(services) errChan := make(chan error) ctx, _ := context.WithTimeout(context.TODO(), time.Millisecond) go n.Start(ctx, errChan) @@ -65,7 +66,7 @@ func TestNode_StartHappy(t *testing.T) { func TestNode_StartContextCancel(t *testing.T) { // create node with two mocked out services services := []Server{&MockService{mustReturnStartErr: false}, &MockService{mustReturnStartErr: false}} - n := NewNode(services) + n := New(services) errChan := make(chan error) ctx, canc := context.WithCancel(context.TODO()) go n.Start(ctx, errChan) @@ -82,7 +83,7 @@ func TestNode_StartContextCancel(t *testing.T) { func TestNode_StartChildError(t *testing.T) { // create node with two mocked out services services := []Server{&MockService{mustReturnStartErr: true}, &MockService{mustReturnStartErr: false}} - n := NewNode(services) + n := New(services) errChan := make(chan error) ctx, _ := context.WithCancel(context.TODO()) go n.Start(ctx, errChan) diff --git a/notification/notification.go b/notification/notification.go index eaf8eb61c..9b620fb95 100644 --- a/notification/notification.go +++ b/notification/notification.go @@ -1,8 +1,10 @@ package notification import ( + "net/http" + "github.com/centrifuge/centrifuge-protobufs/gen/go/notification" - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/utils" "github.com/golang/protobuf/jsonpb" logging "github.com/ipfs/go-log" @@ -10,24 +12,43 @@ import ( var log = logging.Logger("notification-api") +// EventType is the type of the notification. type EventType int +// Status defines the status of the notification. type Status int +// Constants defined for notification delivery. const ( ReceivedPayload EventType = 1 Failure Status = 0 Success Status = 1 ) +// Config defines methods required for this package. +type Config interface { + GetReceiveEventNotificationEndpoint() string +} + +// Sender defines methods that can handle a notification. type Sender interface { Send(notification *notificationpb.NotificationMessage) (Status, error) } -type WebhookSender struct{} +// NewWebhookSender returns an implementation of a Sender that sends notifications through webhooks. +func NewWebhookSender(config Config) Sender { + return webhookSender{config} +} + +// NewWebhookSender implements Sender. +// Sends notification through a webhook defined. +type webhookSender struct { + config Config +} -func (wh *WebhookSender) Send(notification *notificationpb.NotificationMessage) (Status, error) { - url := config.Config.GetReceiveEventNotificationEndpoint() +// Send sends notification to the defined webhook. +func (wh webhookSender) Send(notification *notificationpb.NotificationMessage) (Status, error) { + url := wh.config.GetReceiveEventNotificationEndpoint() if url == "" { log.Warningf("Webhook URL not defined, manually fetch received document") return Success, nil @@ -35,21 +56,24 @@ func (wh *WebhookSender) Send(notification *notificationpb.NotificationMessage) payload, err := wh.constructPayload(notification) if err != nil { - log.Error(err) return Failure, err } - httpStatusCode, err := utils.SendPOSTRequest(url, "application/json", payload) - if httpStatusCode != 200 { + statusCode, err := utils.SendPOSTRequest(url, "application/json", payload) + if err != nil { return Failure, err } + if statusCode != http.StatusOK { + return Failure, errors.New("failed to send webhook: status = %v", statusCode) + } + log.Infof("Sent Webhook Notification with Payload [%v] to [%s]", notification, url) return Success, nil } -func (wh *WebhookSender) constructPayload(notification *notificationpb.NotificationMessage) ([]byte, error) { +func (wh webhookSender) constructPayload(notification *notificationpb.NotificationMessage) ([]byte, error) { marshaler := jsonpb.Marshaler{} payload, err := marshaler.MarshalToString(notification) if err != nil { diff --git a/notification/notification_test.go b/notification/notification_test.go index c2df4e10c..7b8d55012 100644 --- a/notification/notification_test.go +++ b/notification/notification_test.go @@ -13,9 +13,8 @@ import ( "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/context/testlogging" - "github.com/centrifuge/go-centrifuge/storage" - "github.com/centrifuge/go-centrifuge/testingutils" "github.com/centrifuge/go-centrifuge/utils" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/ptypes" "github.com/stretchr/testify/assert" @@ -25,7 +24,6 @@ func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, - &storage.Bootstrapper{}, } bootstrap.RunTestBootstrappers(ibootstappers, nil) result := m.Run() @@ -36,19 +34,19 @@ func TestMain(m *testing.M) { func TestWebhookConstructPayload(t *testing.T) { documentIdentifier := utils.RandomSlice(32) coredoc := &coredocumentpb.CoreDocument{DocumentIdentifier: documentIdentifier} - cid := testingutils.Rand32Bytes() + cid := utils.RandomSlice(32) ts, err := ptypes.TimestampProto(time.Now().UTC()) assert.Nil(t, err, "Should not error out") notificationMessage := ¬ificationpb.NotificationMessage{ - DocumentIdentifier: coredoc.DocumentIdentifier, - DocumentType: documenttypes.InvoiceDataTypeUrl, - CentrifugeId: cid, - EventType: uint32(ReceivedPayload), - Recorded: ts, + DocumentId: hexutil.Encode(coredoc.DocumentIdentifier), + DocumentType: documenttypes.InvoiceDataTypeUrl, + CentrifugeId: hexutil.Encode(cid), + EventType: uint32(ReceivedPayload), + Recorded: ts, } - whs := WebhookSender{} + whs := webhookSender{} bresult, err := whs.constructPayload(notificationMessage) assert.Nil(t, err, "Should not error out") @@ -58,7 +56,7 @@ func TestWebhookConstructPayload(t *testing.T) { assert.Equal(t, notificationMessage.Recorded, unmarshaledNotificationMessage.Recorded, "Recorder Timestamp should be equal") assert.Equal(t, notificationMessage.DocumentType, unmarshaledNotificationMessage.DocumentType, "DocumentType should be equal") - assert.Equal(t, notificationMessage.DocumentIdentifier, unmarshaledNotificationMessage.DocumentIdentifier, "DocumentIdentifier should be equal") + assert.Equal(t, notificationMessage.DocumentId, unmarshaledNotificationMessage.DocumentId, "DocumentIdentifier should be equal") assert.Equal(t, notificationMessage.CentrifugeId, unmarshaledNotificationMessage.CentrifugeId, "CentrifugeID should be equal") assert.Equal(t, notificationMessage.EventType, unmarshaledNotificationMessage.EventType, "EventType should be equal") } diff --git a/p2p/bootstrapper.go b/p2p/bootstrapper.go new file mode 100644 index 000000000..e1813b0f5 --- /dev/null +++ b/p2p/bootstrapper.go @@ -0,0 +1,34 @@ +package p2p + +import ( + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" +) + +// Bootstrapped constants that are used as key in bootstrap context +const ( + BootstrappedP2PClient string = "BootstrappedP2PClient" +) + +// Bootstrapper implements Bootstrapper with p2p details +type Bootstrapper struct{} + +// Bootstrap initiates p2p server and client into context +func (b Bootstrapper) Bootstrap(ctx map[string]interface{}) error { + cfg, ok := ctx[bootstrap.BootstrappedConfig].(config.Configuration) + if !ok { + return errors.New("config not initialised") + } + + registry, ok := ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + if !ok { + return errors.New("registry not initialised") + } + + srv := &p2pServer{config: cfg, registry: registry, handler: GRPCHandler(cfg, registry)} + ctx[bootstrap.BootstrappedP2PServer] = srv + ctx[BootstrappedP2PClient] = srv + return nil +} diff --git a/p2p/bootstrapper_test.go b/p2p/bootstrapper_test.go new file mode 100644 index 000000000..7a9ed22d4 --- /dev/null +++ b/p2p/bootstrapper_test.go @@ -0,0 +1,36 @@ +// +build unit + +package p2p + +import ( + "testing" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/node" + "github.com/centrifuge/go-centrifuge/testingutils/config" + "github.com/stretchr/testify/assert" +) + +func TestBootstrapper_Bootstrap(t *testing.T) { + b := Bootstrapper{} + + // no config + m := make(map[string]interface{}) + err := b.Bootstrap(m) + assert.Error(t, err) + + // config + m[bootstrap.BootstrappedConfig] = new(testingconfig.MockConfig) + m[documents.BootstrappedRegistry] = documents.NewServiceRegistry() + err = b.Bootstrap(m) + assert.Nil(t, err) + + assert.NotNil(t, m[bootstrap.BootstrappedP2PServer]) + _, ok := m[bootstrap.BootstrappedP2PServer].(node.Server) + assert.True(t, ok) + + assert.NotNil(t, m[BootstrappedP2PClient]) + _, ok = m[BootstrappedP2PClient].(Client) + assert.True(t, ok) +} diff --git a/p2p/client.go b/p2p/client.go index ab8e9fcc4..fb68191ea 100644 --- a/p2p/client.go +++ b/p2p/client.go @@ -8,10 +8,10 @@ import ( "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/config" "github.com/centrifuge/go-centrifuge/coredocument" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/signatures" "github.com/centrifuge/go-centrifuge/version" "github.com/libp2p/go-libp2p-peer" pstore "github.com/libp2p/go-libp2p-peerstore" @@ -19,20 +19,14 @@ import ( "google.golang.org/grpc" ) +// Client defines methods that can be implemented by any type handling p2p communications. type Client interface { OpenClient(target string) (p2ppb.P2PServiceClient, error) - GetSignaturesForDocument(ctx context.Context, doc *coredocumentpb.CoreDocument) error + GetSignaturesForDocument(ctx *header.ContextHeader, identityService identity.Service, doc *coredocumentpb.CoreDocument) error } -func NewP2PClient() Client { - return &defaultClient{} -} - -type defaultClient struct { -} - -// Opens a client connection with libp2p -func (d *defaultClient) OpenClient(target string) (p2ppb.P2PServiceClient, error) { +// OpenClient returns P2PServiceClient to contact the remote peer +func (s *p2pServer) OpenClient(target string) (p2ppb.P2PServiceClient, error) { log.Info("Opening connection to: %s", target) ipfsAddr, err := ma.NewMultiaddr(target) if err != nil { @@ -51,46 +45,44 @@ func (d *defaultClient) OpenClient(target string) (p2ppb.P2PServiceClient, error // Decapsulate the /ipfs/ part from the target // /ip4//ipfs/ becomes /ip4/ - targetPeerAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerID))) + targetPeerAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", pid)) targetAddr := ipfsAddr.Decapsulate(targetPeerAddr) - hostInstance := GetHost() - grpcProtoInstance := GetGRPCProto() - // We have a peer ID and a targetAddr so we add it to the peer store // so LibP2P knows how to contact it - hostInstance.Peerstore().AddAddr(peerID, targetAddr, pstore.PermanentAddrTTL) + s.host.Peerstore().AddAddr(peerID, targetAddr, pstore.PermanentAddrTTL) // make a new stream from host B to host A with timeout // Retrial is handled internally, connection request will be cancelled by the connection timeout context - ctx, _ := context.WithTimeout(context.Background(), config.Config.GetP2PConnectionTimeout()) - g, err := grpcProtoInstance.Dial(ctx, peerID, grpc.WithInsecure(), grpc.WithBlock()) + ctx, cancel := context.WithTimeout(context.Background(), s.config.GetP2PConnectionTimeout()) + defer cancel() + g, err := s.protocol.Dial(ctx, peerID, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { - return nil, fmt.Errorf("failed to dial peer [%s]: %v", peerID.Pretty(), err) + return nil, errors.New("failed to dial peer [%s]: %v", peerID.Pretty(), err) } return p2ppb.NewP2PServiceClient(g), nil } // getSignatureForDocument requests the target node to sign the document -func getSignatureForDocument(ctx context.Context, doc coredocumentpb.CoreDocument, client p2ppb.P2PServiceClient, receiverCentId identity.CentID) (*p2ppb.SignatureResponse, error) { - senderId, err := config.Config.GetIdentityID() +func (s *p2pServer) getSignatureForDocument(ctx context.Context, identityService identity.Service, doc coredocumentpb.CoreDocument, client p2ppb.P2PServiceClient, receiverCentID identity.CentID) (*p2ppb.SignatureResponse, error) { + senderID, err := s.config.GetIdentityID() if err != nil { return nil, err } - header := p2ppb.CentrifugeHeader{ - NetworkIdentifier: config.Config.GetNetworkID(), + h := p2ppb.CentrifugeHeader{ + NetworkIdentifier: s.config.GetNetworkID(), CentNodeVersion: version.GetVersion().String(), - SenderCentrifugeId: senderId, + SenderCentrifugeId: senderID, } req := &p2ppb.SignatureRequest{ - Header: &header, + Header: &h, Document: &doc, } - log.Infof("Requesting signature from %s\n", receiverCentId) + log.Infof("Requesting signature from %s\n", receiverCentID) resp, err := client.RequestDocumentSignature(ctx, req) if err != nil { @@ -102,18 +94,17 @@ func getSignatureForDocument(ctx context.Context, doc coredocumentpb.CoreDocumen return nil, version.IncompatibleVersionError(resp.CentNodeVersion) } - err = signatures.ValidateCentrifugeID(resp.Signature, receiverCentId) + err = identity.ValidateCentrifugeIDBytes(resp.Signature.EntityId, receiverCentID) if err != nil { return nil, centerrors.New(code.AuthenticationFailed, err.Error()) } - err = signatures.ValidateSignature(resp.Signature, doc.SigningRoot) + err = identityService.ValidateSignature(resp.Signature, doc.SigningRoot) if err != nil { return nil, centerrors.New(code.AuthenticationFailed, "signature invalid") } - log.Infof("Signature successfully received from %s\n", receiverCentId) - + log.Infof("Signature successfully received from %s\n", receiverCentID) return resp, nil } @@ -122,8 +113,8 @@ type signatureResponseWrap struct { err error } -func getSignatureAsync(ctx context.Context, doc coredocumentpb.CoreDocument, client p2ppb.P2PServiceClient, receiverCentId identity.CentID, out chan<- signatureResponseWrap) { - resp, err := getSignatureForDocument(ctx, doc, client, receiverCentId) +func (s *p2pServer) getSignatureAsync(ctx context.Context, identityService identity.Service, doc coredocumentpb.CoreDocument, client p2ppb.P2PServiceClient, receiverCentID identity.CentID, out chan<- signatureResponseWrap) { + resp, err := s.getSignatureForDocument(ctx, identityService, doc, client, receiverCentID) out <- signatureResponseWrap{ resp: resp, err: err, @@ -131,11 +122,11 @@ func getSignatureAsync(ctx context.Context, doc coredocumentpb.CoreDocument, cli } // GetSignaturesForDocument requests peer nodes for the signature and verifies them -func (d *defaultClient) GetSignaturesForDocument(ctx context.Context, doc *coredocumentpb.CoreDocument) error { +func (s *p2pServer) GetSignaturesForDocument(ctx *header.ContextHeader, identityService identity.Service, doc *coredocumentpb.CoreDocument) error { in := make(chan signatureResponseWrap) defer close(in) - extCollaborators, err := coredocument.GetExternalCollaborators(doc) + extCollaborators, err := coredocument.GetExternalCollaborators(ctx.Self().ID, doc) if err != nil { return centerrors.Wrap(err, "failed to get external collaborators") } @@ -146,13 +137,13 @@ func (d *defaultClient) GetSignaturesForDocument(ctx context.Context, doc *cored if err != nil { return centerrors.Wrap(err, "failed to convert to CentID") } - target, err := identity.GetClientP2PURL(collaboratorID) + target, err := identityService.GetClientP2PURL(collaboratorID) if err != nil { return centerrors.Wrap(err, "failed to get P2P url") } - client, err := d.OpenClient(target) + client, err := s.OpenClient(target) if err != nil { log.Error(centerrors.Wrap(err, "failed to connect to target")) continue @@ -161,7 +152,8 @@ func (d *defaultClient) GetSignaturesForDocument(ctx context.Context, doc *cored // for now going with context.background, once we have a timeout for request // we can use context.Timeout for that count++ - go getSignatureAsync(ctx, *doc, client, collaboratorID, in) + c, _ := context.WithTimeout(ctx.Context(), s.config.GetP2PConnectionTimeout()) + go s.getSignatureAsync(c, identityService, *doc, client, collaboratorID, in) } var responses []signatureResponseWrap diff --git a/p2p/client_test.go b/p2p/client_test.go index 435240d2e..fb5e7681e 100644 --- a/p2p/client_test.go +++ b/p2p/client_test.go @@ -4,46 +4,29 @@ package p2p import ( "context" - "fmt" - "os" "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/context/testlogging" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/storage" - "github.com/centrifuge/go-centrifuge/testingutils" "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/go-centrifuge/version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func TestMain(m *testing.M) { - ibootstappers := []bootstrap.TestBootstrapper{ - &testlogging.TestLoggingBootstrapper{}, - &config.Bootstrapper{}, - &storage.Bootstrapper{}, - } - bootstrap.RunTestBootstrappers(ibootstappers, nil) - result := m.Run() - bootstrap.RunTestTeardown(ibootstappers) - os.Exit(result) -} - func TestGetSignatureForDocument_fail_connect(t *testing.T) { client := &testingcommons.P2PMockClient{} - coreDoc := testingutils.GenerateCoreDocument() + coreDoc := testingcoredocument.GenerateCoreDocument() ctx := context.Background() centrifugeId, err := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) assert.Nil(t, err, "centrifugeId not initialized correctly ") - client.On("RequestDocumentSignature", ctx, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("signature failed")).Once() - resp, err := getSignatureForDocument(ctx, *coreDoc, client, centrifugeId) + client.On("RequestDocumentSignature", ctx, mock.Anything, mock.Anything).Return(nil, errors.New("signature failed")).Once() + resp, err := testClient.getSignatureForDocument(ctx, nil, *coreDoc, client, centrifugeId) client.AssertExpectations(t) assert.Error(t, err, "must fail") assert.Nil(t, resp, "must be nil") @@ -51,7 +34,7 @@ func TestGetSignatureForDocument_fail_connect(t *testing.T) { func TestGetSignatureForDocument_fail_version_check(t *testing.T) { client := &testingcommons.P2PMockClient{} - coreDoc := testingutils.GenerateCoreDocument() + coreDoc := testingcoredocument.GenerateCoreDocument() ctx := context.Background() resp := &p2ppb.SignatureResponse{CentNodeVersion: "1.0.0"} @@ -59,7 +42,7 @@ func TestGetSignatureForDocument_fail_version_check(t *testing.T) { assert.Nil(t, err, "centrifugeId not initialized correctly ") client.On("RequestDocumentSignature", ctx, mock.Anything, mock.Anything).Return(resp, nil).Once() - resp, err = getSignatureForDocument(ctx, *coreDoc, client, centrifugeId) + resp, err = testClient.getSignatureForDocument(ctx, nil, *coreDoc, client, centrifugeId) client.AssertExpectations(t) assert.Error(t, err, "must fail") assert.Contains(t, err.Error(), "Incompatible version") @@ -68,7 +51,7 @@ func TestGetSignatureForDocument_fail_version_check(t *testing.T) { func TestGetSignatureForDocument_fail_centrifugeId(t *testing.T) { client := &testingcommons.P2PMockClient{} - coreDoc := testingutils.GenerateCoreDocument() + coreDoc := testingcoredocument.GenerateCoreDocument() ctx := context.Background() centrifugeId, err := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) @@ -83,11 +66,11 @@ func TestGetSignatureForDocument_fail_centrifugeId(t *testing.T) { } client.On("RequestDocumentSignature", ctx, mock.Anything, mock.Anything).Return(sigResp, nil).Once() - resp, err := getSignatureForDocument(ctx, *coreDoc, client, centrifugeId) + resp, err := testClient.getSignatureForDocument(ctx, nil, *coreDoc, client, centrifugeId) client.AssertExpectations(t) assert.Nil(t, resp, "must be nil") assert.Error(t, err, "must not be nil") - assert.Contains(t, err.Error(), "[5]signature entity doesn't match provided centID") + assert.Contains(t, err.Error(), "[5]provided bytes doesn't match centID") } diff --git a/p2p/p2phandler/handler.go b/p2p/handler.go similarity index 50% rename from p2p/p2phandler/handler.go rename to p2p/handler.go index 21b637ab7..32493347d 100644 --- a/p2p/p2phandler/handler.go +++ b/p2p/handler.go @@ -1,4 +1,4 @@ -package p2phandler +package p2p import ( "context" @@ -8,56 +8,69 @@ import ( "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/config" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/notification" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/header" "github.com/centrifuge/go-centrifuge/version" ) // getService looks up the specific registry, derives service from core document -func getServiceAndModel(cd *coredocumentpb.CoreDocument) (documents.Service, documents.Model, error) { +func getServiceAndModel(registry *documents.ServiceRegistry, cd *coredocumentpb.CoreDocument) (documents.Service, documents.Model, error) { if cd == nil { - return nil, nil, fmt.Errorf("nil core document") + return nil, nil, errors.New("nil core document") } docType, err := coredocument.GetTypeURL(cd) if err != nil { - return nil, nil, fmt.Errorf("failed to get type of the document: %v", err) + return nil, nil, errors.New("failed to get type of the document: %v", err) } - srv, err := documents.GetRegistryInstance().LocateService(docType) + srv, err := registry.LocateService(docType) if err != nil { - return nil, nil, fmt.Errorf("failed to locate the service: %v", err) + return nil, nil, errors.New("failed to locate the service: %v", err) } model, err := srv.DeriveFromCoreDocument(cd) if err != nil { - return nil, nil, fmt.Errorf("failed to derive model from core document: %v", err) + return nil, nil, errors.New("failed to derive model from core document: %v", err) } return srv, model, nil } -// Handler implements the grpc interface -type Handler struct { - Notifier notification.Sender +// handler implements the grpc interface +type handler struct { + registry *documents.ServiceRegistry + config config.Configuration +} + +// GRPCHandler returns an implementation of P2PServiceServer +func GRPCHandler(config config.Configuration, registry *documents.ServiceRegistry) p2ppb.P2PServiceServer { + return handler{registry: registry, config: config} } // RequestDocumentSignature signs the received document and returns the signature of the signingRoot // 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) { - err := handshakeValidator().Validate(sigReq.Header) +func (srv handler) RequestDocumentSignature(ctx context.Context, sigReq *p2ppb.SignatureRequest) (*p2ppb.SignatureResponse, error) { + ctxHeader, err := header.NewContextHeader(ctx, srv.config) + if err != nil { + log.Error(err) + return nil, centerrors.New(code.Unknown, fmt.Sprintf("failed to get header: %v", err)) + } + err = handshakeValidator(srv.config.GetNetworkID()).Validate(sigReq.Header) if err != nil { return nil, err } - svc, model, err := getServiceAndModel(sigReq.Document) + svc, model, err := getServiceAndModel(srv.registry, sigReq.Document) if err != nil { return nil, centerrors.New(code.DocumentInvalid, err.Error()) } - signature, err := svc.RequestDocumentSignature(model) + signature, err := svc.RequestDocumentSignature(ctxHeader, model) if err != nil { return nil, centerrors.New(code.Unknown, err.Error()) } @@ -69,13 +82,13 @@ func (srv *Handler) RequestDocumentSignature(ctx context.Context, sigReq *p2ppb. } // SendAnchoredDocument receives a new anchored document, validates and updates the document in DB -func (srv *Handler) SendAnchoredDocument(ctx context.Context, docReq *p2ppb.AnchorDocumentRequest) (*p2ppb.AnchorDocumentResponse, error) { - err := handshakeValidator().Validate(docReq.Header) +func (srv handler) SendAnchoredDocument(ctx context.Context, docReq *p2ppb.AnchorDocumentRequest) (*p2ppb.AnchorDocumentResponse, error) { + err := handshakeValidator(srv.config.GetNetworkID()).Validate(docReq.Header) if err != nil { return nil, err } - svc, model, err := getServiceAndModel(docReq.Document) + svc, model, err := getServiceAndModel(srv.registry, docReq.Document) if err != nil { return nil, centerrors.New(code.DocumentInvalid, err.Error()) } diff --git a/p2p/p2phandler/handler_integration_test.go b/p2p/handler_integration_test.go similarity index 60% rename from p2p/p2phandler/handler_integration_test.go rename to p2p/handler_integration_test.go index 6679696d7..6f6c2bc1f 100644 --- a/p2p/p2phandler/handler_integration_test.go +++ b/p2p/handler_integration_test.go @@ -1,10 +1,10 @@ // +build integration -package p2phandler_test +package p2p_test import ( "context" - "math/big" + "flag" "os" "testing" @@ -12,17 +12,16 @@ import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "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/config" cc "github.com/centrifuge/go-centrifuge/context/testingbootstrap" "github.com/centrifuge/go-centrifuge/coredocument" + "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/identity" - cented25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/centrifuge/go-centrifuge/keytools/secp256k1" - "github.com/centrifuge/go-centrifuge/notification" - "github.com/centrifuge/go-centrifuge/p2p/p2phandler" - "github.com/centrifuge/go-centrifuge/signatures" - "github.com/centrifuge/go-centrifuge/testingutils" - "github.com/centrifuge/go-centrifuge/testingutils/commons" + "github.com/centrifuge/go-centrifuge/p2p" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" + "github.com/centrifuge/go-centrifuge/testingutils/identity" "github.com/centrifuge/go-centrifuge/utils" "github.com/centrifuge/go-centrifuge/version" "github.com/centrifuge/precise-proofs/proofs" @@ -31,10 +30,26 @@ import ( "golang.org/x/crypto/ed25519" ) -var handler = p2phandler.Handler{Notifier: ¬ification.WebhookSender{}} +var ( + handler p2ppb.P2PServiceServer + anchorRepo anchors.AnchorRepository + cfg config.Configuration + idService identity.Service +) func TestMain(m *testing.M) { - cc.TestFunctionalEthereumBootstrap() + flag.Parse() + ctx := cc.TestFunctionalEthereumBootstrap() + registry := ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + anchorRepo = ctx[anchors.BootstrappedAnchorRepo].(anchors.AnchorRepository) + idService = ctx[identity.BootstrappedIDService].(identity.Service) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + handler = p2p.GRPCHandler(cfg, registry) + testingidentity.CreateIdentityWithKeys(cfg, idService) result := m.Run() cc.TestFunctionalEthereumTearDown() os.Exit(result) @@ -51,129 +66,66 @@ func TestHandler_RequestDocumentSignature_verification_fail(t *testing.T) { } func TestHandler_RequestDocumentSignature_AlreadyExists(t *testing.T) { - savedService := identity.IDService - idConfig, err := cented25519.GetIDConfig() - assert.Nil(t, err) - centID, _ := identity.ToCentID(idConfig.ID) - pubKey := idConfig.PublicKey - b32Key, _ := utils.SliceToByte32(pubKey) - idkey := &identity.EthereumIdentityKey{ - Key: b32Key, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - identity.IDService = srv - doc := prepareDocumentForP2PHandler(t, nil) req := getSignatureRequest(doc) resp, err := handler.RequestDocumentSignature(context.Background(), req) - srv.AssertExpectations(t) - id.AssertExpectations(t) assert.Nil(t, err, "must be nil") assert.NotNil(t, resp, "must be non nil") - id = &testingcommons.MockID{} - srv = &testingcommons.MockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - identity.IDService = srv - req = getSignatureRequest(doc) resp, err = handler.RequestDocumentSignature(context.Background(), req) - srv.AssertExpectations(t) - id.AssertExpectations(t) assert.NotNil(t, err, "must not be nil") - assert.Contains(t, err.Error(), "document already exists") - - identity.IDService = savedService + assert.Contains(t, err.Error(), documents.ErrDocumentRepositoryModelAllReadyExists.Error()) } func TestHandler_RequestDocumentSignature_UpdateSucceeds(t *testing.T) { - savedService := identity.IDService - idConfig, err := cented25519.GetIDConfig() - assert.Nil(t, err) - centID, _ := identity.ToCentID(idConfig.ID) - pubKey := idConfig.PublicKey - b32Key, _ := utils.SliceToByte32(pubKey) - idkey := &identity.EthereumIdentityKey{ - Key: b32Key, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - identity.IDService = srv - doc := prepareDocumentForP2PHandler(t, nil) req := getSignatureRequest(doc) resp, err := handler.RequestDocumentSignature(context.Background(), req) - srv.AssertExpectations(t) - id.AssertExpectations(t) 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, ed25519.Verify(sig.PublicKey, doc.SigningRoot, sig.Signature), "signature must be valid") - //Update document newDoc, err := coredocument.PrepareNewVersion(*doc, nil) assert.Nil(t, err) - id = &testingcommons.MockID{} - srv = &testingcommons.MockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - identity.IDService = srv - updateDocumentForP2Phandler(t, newDoc) newDoc = prepareDocumentForP2PHandler(t, newDoc) req = getSignatureRequest(newDoc) resp, err = handler.RequestDocumentSignature(context.Background(), req) - srv.AssertExpectations(t) - id.AssertExpectations(t) 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, ed25519.Verify(sig.PublicKey, newDoc.SigningRoot, sig.Signature), "signature must be valid") - identity.IDService = savedService } -func TestHandler_RequestDocumentSignature(t *testing.T) { - savedService := identity.IDService - idConfig, err := cented25519.GetIDConfig() +func TestHandler_RequestDocumentSignatureFirstTimeOnUpdatedDocument(t *testing.T) { + doc := prepareDocumentForP2PHandler(t, nil) + newDoc, err := coredocument.PrepareNewVersion(*doc, nil) assert.Nil(t, err) - centID, _ := identity.ToCentID(idConfig.ID) - pubKey := idConfig.PublicKey - b32Key, _ := utils.SliceToByte32(pubKey) - idkey := &identity.EthereumIdentityKey{ - Key: b32Key, - Purposes: []*big.Int{big.NewInt(identity.KeyPurposeSigning)}, - RevokedAt: big.NewInt(0), - } - id := &testingcommons.MockID{} - srv := &testingcommons.MockIDService{} - srv.On("LookupIdentityForID", centID).Return(id, nil).Once() - id.On("FetchKey", pubKey).Return(idkey, nil).Once() - identity.IDService = srv + assert.NotEqual(t, newDoc.DocumentIdentifier, newDoc.CurrentVersion) + updateDocumentForP2Phandler(t, newDoc) + newDoc = prepareDocumentForP2PHandler(t, newDoc) + req := getSignatureRequest(newDoc) + resp, err := handler.RequestDocumentSignature(context.Background(), req) + 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, ed25519.Verify(sig.PublicKey, newDoc.SigningRoot, sig.Signature), "signature must be valid") +} +func TestHandler_RequestDocumentSignature(t *testing.T) { doc := prepareDocumentForP2PHandler(t, nil) req := getSignatureRequest(doc) resp, err := handler.RequestDocumentSignature(context.Background(), req) - srv.AssertExpectations(t) - id.AssertExpectations(t) 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, ed25519.Verify(sig.PublicKey, doc.SigningRoot, sig.Signature), "signature must be valid") - - identity.IDService = savedService } func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { @@ -182,12 +134,12 @@ func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { doc := prepareDocumentForP2PHandler(t, nil) // Anchor document - secpIDConfig, err := secp256k1.GetIDConfig() - anchorIDTyped, _ := anchors.NewAnchorID(doc.CurrentVersion) - docRootTyped, _ := anchors.NewDocRoot(doc.DocumentRoot) + idConfig, err := identity.GetIdentityConfig(cfg) + anchorIDTyped, _ := anchors.ToAnchorID(doc.CurrentVersion) + docRootTyped, _ := anchors.ToDocumentRoot(doc.DocumentRoot) messageToSign := anchors.GenerateCommitHash(anchorIDTyped, centrifugeId, docRootTyped) - signature, _ := secp256k1.SignEthereum(messageToSign, secpIDConfig.PrivateKey) - anchorConfirmations, err := anchors.CommitAnchor(anchorIDTyped, docRootTyped, centrifugeId, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, signature) + signature, _ := secp256k1.SignEthereum(messageToSign, idConfig.Keys[identity.KeyPurposeEthMsgAuth].PrivateKey) + anchorConfirmations, err := anchorRepo.CommitAnchor(anchorIDTyped, docRootTyped, centrifugeId, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, signature) assert.Nil(t, err) watchCommittedAnchor := <-anchorConfirmations @@ -196,7 +148,7 @@ func TestHandler_SendAnchoredDocument_update_fail(t *testing.T) { anchorReq := getAnchoredRequest(doc) anchorResp, err := handler.SendAnchoredDocument(context.Background(), anchorReq) assert.Error(t, err) - assert.Contains(t, err.Error(), "document doesn't exists") + assert.Contains(t, err.Error(), documents.ErrDocumentRepositoryModelDoesntExist.Error()) assert.Nil(t, anchorResp) } @@ -224,12 +176,12 @@ func TestHandler_SendAnchoredDocument(t *testing.T) { doc.DocumentRoot = tree.RootHash() // Anchor document - secpIDConfig, err := secp256k1.GetIDConfig() - anchorIDTyped, _ := anchors.NewAnchorID(doc.CurrentVersion) - docRootTyped, _ := anchors.NewDocRoot(doc.DocumentRoot) + idConfig, err := identity.GetIdentityConfig(cfg) + anchorIDTyped, _ := anchors.ToAnchorID(doc.CurrentVersion) + docRootTyped, _ := anchors.ToDocumentRoot(doc.DocumentRoot) messageToSign := anchors.GenerateCommitHash(anchorIDTyped, centrifugeId, docRootTyped) - signature, _ := secp256k1.SignEthereum(messageToSign, secpIDConfig.PrivateKey) - anchorConfirmations, err := anchors.CommitAnchor(anchorIDTyped, docRootTyped, centrifugeId, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, signature) + signature, _ := secp256k1.SignEthereum(messageToSign, idConfig.Keys[identity.KeyPurposeEthMsgAuth].PrivateKey) + anchorConfirmations, err := anchorRepo.CommitAnchor(anchorIDTyped, docRootTyped, centrifugeId, [][anchors.DocumentProofLength]byte{utils.RandomByte32()}, signature) assert.Nil(t, err) watchCommittedAnchor := <-anchorConfirmations @@ -246,46 +198,41 @@ func TestHandler_SendAnchoredDocument(t *testing.T) { func createIdentity(t *testing.T) identity.CentID { // Create Identity centrifugeId, _ := identity.ToCentID(utils.RandomSlice(identity.CentIDLength)) - config.Config.V.Set("identityId", centrifugeId.String()) - id, confirmations, err := identity.IDService.CreateIdentity(centrifugeId) + cfg.Set("identityId", centrifugeId.String()) + id, confirmations, err := idService.CreateIdentity(centrifugeId) assert.Nil(t, err, "should not error out when creating identity") watchRegisteredIdentity := <-confirmations assert.Nil(t, watchRegisteredIdentity.Error, "No error thrown by context") - assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchRegisteredIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") + idConfig, err := identity.GetIdentityConfig(cfg) // Add Keys - idConfig, err := cented25519.GetIDConfig() - pubKey := idConfig.PublicKey + pubKey := idConfig.Keys[identity.KeyPurposeSigning].PublicKey confirmations, err = id.AddKeyToIdentity(context.Background(), identity.KeyPurposeSigning, pubKey) assert.Nil(t, err, "should not error out when adding key to identity") assert.NotNil(t, confirmations, "confirmations channel should not be nil") watchReceivedIdentity := <-confirmations - assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") - secpIDConfig, err := secp256k1.GetIDConfig() - secPubKey := secpIDConfig.PublicKey + secPubKey := idConfig.Keys[identity.KeyPurposeEthMsgAuth].PublicKey confirmations, err = id.AddKeyToIdentity(context.Background(), identity.KeyPurposeEthMsgAuth, secPubKey) assert.Nil(t, err, "should not error out when adding key to identity") assert.NotNil(t, confirmations, "confirmations channel should not be nil") watchReceivedIdentity = <-confirmations - assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.GetCentrifugeID(), "Resulting Identity should have the same ID as the input") + assert.Equal(t, centrifugeId, watchReceivedIdentity.Identity.CentID(), "Resulting Identity should have the same ID as the input") return centrifugeId } func prepareDocumentForP2PHandler(t *testing.T, doc *coredocumentpb.CoreDocument) *coredocumentpb.CoreDocument { - idConfig, err := cented25519.GetIDConfig() + idConfig, err := identity.GetIdentityConfig(cfg) assert.Nil(t, err) if doc == nil { - doc = testingutils.GenerateCoreDocument() + doc = testingcoredocument.GenerateCoreDocument() } tree, _ := coredocument.GetDocumentSigningTree(doc) doc.SigningRoot = tree.RootHash() - sig := signatures.Sign(&config.IdentityConfig{ - ID: idConfig.ID, - PublicKey: idConfig.PublicKey, - PrivateKey: idConfig.PrivateKey, - }, doc.SigningRoot) + sig := identity.Sign(idConfig, identity.KeyPurposeSigning, doc.SigningRoot) doc.Signatures = append(doc.Signatures, sig) tree, _ = coredocument.GetDocumentRootTree(doc) doc.DocumentRoot = tree.RootHash() @@ -310,13 +257,13 @@ func getAnchoredRequest(doc *coredocumentpb.CoreDocument) *p2ppb.AnchorDocumentR return &p2ppb.AnchorDocumentRequest{ Header: &p2ppb.CentrifugeHeader{ CentNodeVersion: version.GetVersion().String(), - NetworkIdentifier: config.Config.GetNetworkID(), + NetworkIdentifier: cfg.GetNetworkID(), }, Document: doc, } } func getSignatureRequest(doc *coredocumentpb.CoreDocument) *p2ppb.SignatureRequest { return &p2ppb.SignatureRequest{Header: &p2ppb.CentrifugeHeader{ - CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: config.Config.GetNetworkID(), + CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: cfg.GetNetworkID(), }, Document: doc} } diff --git a/p2p/p2phandler/handler_test.go b/p2p/handler_test.go similarity index 66% rename from p2p/p2phandler/handler_test.go rename to p2p/handler_test.go index 47bc1d483..ce204e8ee 100644 --- a/p2p/p2phandler/handler_test.go +++ b/p2p/handler_test.go @@ -1,16 +1,15 @@ // +build unit -package p2phandler +package p2p import ( "context" - "fmt" - "os" "strconv" "testing" + "os" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/centrifuge-protobufs/gen/go/notification" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/bootstrap" "github.com/centrifuge/go-centrifuge/centerrors" @@ -19,9 +18,9 @@ import ( "github.com/centrifuge/go-centrifuge/context/testlogging" "github.com/centrifuge/go-centrifuge/coredocument" "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/notification" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/storage" - "github.com/centrifuge/go-centrifuge/testingutils" + "github.com/centrifuge/go-centrifuge/testingutils/coredocument" "github.com/centrifuge/go-centrifuge/version" "github.com/golang/protobuf/ptypes/any" "github.com/stretchr/testify/assert" @@ -29,46 +28,51 @@ import ( ) var ( - handler = Handler{Notifier: &MockWebhookSender{}} + grpcHandler p2ppb.P2PServiceServer + registry *documents.ServiceRegistry + coreDoc = testingcoredocument.GenerateCoreDocument() + cfg config.Configuration + testClient *p2pServer ) -// MockWebhookSender implements notification.Sender -type MockWebhookSender struct{} - -func (wh *MockWebhookSender) Send(notification *notificationpb.NotificationMessage) (status notification.Status, err error) { - return -} - func TestMain(m *testing.M) { ibootstappers := []bootstrap.TestBootstrapper{ &testlogging.TestLoggingBootstrapper{}, &config.Bootstrapper{}, &storage.Bootstrapper{}, + documents.Bootstrapper{}, } - bootstrap.RunTestBootstrappers(ibootstappers, nil) + ctx := make(map[string]interface{}) + bootstrap.RunTestBootstrappers(ibootstappers, ctx) + cfg = ctx[bootstrap.BootstrappedConfig].(config.Configuration) + cfg.Set("keys.signing.publicKey", "../build/resources/signingKey.pub.pem") + cfg.Set("keys.signing.privateKey", "../build/resources/signingKey.key.pem") + cfg.Set("keys.ethauth.publicKey", "../build/resources/ethauth.pub.pem") + cfg.Set("keys.ethauth.privateKey", "../build/resources/ethauth.key.pem") + registry = ctx[documents.BootstrappedRegistry].(*documents.ServiceRegistry) + grpcHandler = GRPCHandler(cfg, registry) + testClient = &p2pServer{config: cfg} result := m.Run() bootstrap.RunTestTeardown(ibootstappers) os.Exit(result) } -var coreDoc = testingutils.GenerateCoreDocument() - func TestHandler_RequestDocumentSignature_nilDocument(t *testing.T) { req := &p2ppb.SignatureRequest{Header: &p2ppb.CentrifugeHeader{ - CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: config.Config.GetNetworkID(), + CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: cfg.GetNetworkID(), }} - resp, err := handler.RequestDocumentSignature(context.Background(), req) + resp, err := grpcHandler.RequestDocumentSignature(context.Background(), req) assert.Error(t, err, "must return error") assert.Nil(t, resp, "must be nil") } func TestHandler_RequestDocumentSignature_version_fail(t *testing.T) { req := &p2ppb.SignatureRequest{Header: &p2ppb.CentrifugeHeader{ - CentNodeVersion: "1000.0.1-invalid", NetworkIdentifier: config.Config.GetNetworkID(), + CentNodeVersion: "1000.0.1-invalid", NetworkIdentifier: cfg.GetNetworkID(), }} - resp, err := handler.RequestDocumentSignature(context.Background(), req) + resp, err := grpcHandler.RequestDocumentSignature(context.Background(), req) assert.Error(t, err, "must return error") assert.Contains(t, err.Error(), "Incompatible version") assert.Nil(t, resp, "must be nil") @@ -78,19 +82,19 @@ func TestSendAnchoredDocument_IncompatibleRequest(t *testing.T) { // Test invalid version header := &p2ppb.CentrifugeHeader{ CentNodeVersion: "1000.0.0-invalid", - NetworkIdentifier: config.Config.GetNetworkID(), + NetworkIdentifier: cfg.GetNetworkID(), } req := p2ppb.AnchorDocumentRequest{Document: coreDoc, Header: header} - res, err := handler.SendAnchoredDocument(context.Background(), &req) + res, err := grpcHandler.SendAnchoredDocument(context.Background(), &req) assert.Error(t, err) p2perr, _ := centerrors.FromError(err) assert.Contains(t, p2perr.Message(), strconv.Itoa(int(code.VersionMismatch))) assert.Nil(t, res) // Test invalid network - header.NetworkIdentifier = config.Config.GetNetworkID() + 1 + header.NetworkIdentifier = cfg.GetNetworkID() + 1 header.CentNodeVersion = version.GetVersion().String() - res, err = handler.SendAnchoredDocument(context.Background(), &req) + res, err = grpcHandler.SendAnchoredDocument(context.Background(), &req) assert.Error(t, err) p2perr, _ = centerrors.FromError(err) assert.Contains(t, p2perr.Message(), strconv.Itoa(int(code.NetworkMismatch))) @@ -100,10 +104,10 @@ func TestSendAnchoredDocument_IncompatibleRequest(t *testing.T) { func TestSendAnchoredDocument_NilDocument(t *testing.T) { header := &p2ppb.CentrifugeHeader{ CentNodeVersion: version.GetVersion().String(), - NetworkIdentifier: config.Config.GetNetworkID(), + NetworkIdentifier: cfg.GetNetworkID(), } req := p2ppb.AnchorDocumentRequest{Header: header} - res, err := handler.SendAnchoredDocument(context.Background(), &req) + res, err := grpcHandler.SendAnchoredDocument(context.Background(), &req) assert.Error(t, err) assert.Nil(t, res) @@ -113,12 +117,12 @@ func TestHandler_SendAnchoredDocument_getServiceAndModel_fail(t *testing.T) { req := &p2ppb.AnchorDocumentRequest{ Header: &p2ppb.CentrifugeHeader{ CentNodeVersion: version.GetVersion().String(), - NetworkIdentifier: config.Config.GetNetworkID(), + NetworkIdentifier: cfg.GetNetworkID(), }, Document: coredocument.New(), } - res, err := handler.SendAnchoredDocument(context.Background(), req) + res, err := grpcHandler.SendAnchoredDocument(context.Background(), req) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get type of the document") assert.Nil(t, res) @@ -131,21 +135,21 @@ func TestP2PService_basicChecks(t *testing.T) { }{ { header: &p2ppb.CentrifugeHeader{CentNodeVersion: "someversion", NetworkIdentifier: 12}, - err: documents.AppendError(version.IncompatibleVersionError("someversion"), incompatibleNetworkError(config.Config.GetNetworkID(), 12)), + err: errors.AppendError(version.IncompatibleVersionError("someversion"), incompatibleNetworkError(cfg.GetNetworkID(), 12)), }, { header: &p2ppb.CentrifugeHeader{CentNodeVersion: "0.0.1", NetworkIdentifier: 12}, - err: documents.AppendError(incompatibleNetworkError(config.Config.GetNetworkID(), 12), nil), + err: errors.AppendError(incompatibleNetworkError(cfg.GetNetworkID(), 12), nil), }, { - header: &p2ppb.CentrifugeHeader{CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: config.Config.GetNetworkID()}, + header: &p2ppb.CentrifugeHeader{CentNodeVersion: version.GetVersion().String(), NetworkIdentifier: cfg.GetNetworkID()}, }, } for _, c := range tests { - err := handshakeValidator().Validate(c.header) + err := handshakeValidator(cfg.GetNetworkID()).Validate(c.header) if err != nil { if c.err == nil { t.Fatalf("unexpected error: %v\n", err) @@ -157,16 +161,6 @@ func TestP2PService_basicChecks(t *testing.T) { } -type mockRepo struct { - mock.Mock - documents.Repository -} - -func (r mockRepo) Update(id []byte, m documents.Model) error { - args := r.Called(id, m) - return args.Error(0) -} - type mockModel struct { mock.Mock documents.Model @@ -185,12 +179,12 @@ func (s mockService) DeriveFromCoreDocument(cd *coredocumentpb.CoreDocument) (do func Test_getServiceAndModel(t *testing.T) { // document nil fail - s, m, err := getServiceAndModel(nil) + s, m, err := getServiceAndModel(registry, nil) assert.Error(t, err) // docType fetch fail cd := coredocument.New() - s, m, err = getServiceAndModel(cd) + s, m, err = getServiceAndModel(registry, cd) assert.Error(t, err) assert.Nil(t, s) assert.Nil(t, m) @@ -201,19 +195,18 @@ func Test_getServiceAndModel(t *testing.T) { TypeUrl: "model_type_fail", Value: []byte("some data"), } - s, m, err = getServiceAndModel(cd) + s, m, err = getServiceAndModel(registry, cd) assert.Error(t, err) assert.Nil(t, s) assert.Nil(t, m) assert.Contains(t, err.Error(), "failed to locate the service") // derive fails - reg := documents.GetRegistryInstance() srv := mockService{} - srv.On("DeriveFromCoreDocument", cd).Return(nil, fmt.Errorf("error")).Once() - err = reg.Register(cd.EmbeddedData.TypeUrl, srv) + srv.On("DeriveFromCoreDocument", cd).Return(nil, errors.New("error")).Once() + err = registry.Register(cd.EmbeddedData.TypeUrl, srv) assert.Nil(t, err) - s, m, err = getServiceAndModel(cd) + s, m, err = getServiceAndModel(registry, cd) srv.AssertExpectations(t) assert.Error(t, err) assert.Nil(t, s) @@ -225,9 +218,9 @@ func Test_getServiceAndModel(t *testing.T) { cd.EmbeddedData.TypeUrl = "get_model_type" srv = mockService{} srv.On("DeriveFromCoreDocument", cd).Return(model, nil).Once() - err = reg.Register(cd.EmbeddedData.TypeUrl, srv) + err = registry.Register(cd.EmbeddedData.TypeUrl, srv) assert.Nil(t, err) - s, m, err = getServiceAndModel(cd) + s, m, err = getServiceAndModel(registry, cd) srv.AssertExpectations(t) assert.Nil(t, err) assert.NotNil(t, s) diff --git a/p2p/server.go b/p2p/server.go index 9663d911b..dd921594f 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -1,17 +1,16 @@ -// PLEASE DO NOT call any config.* stuff here as it creates dependencies that can't be injected easily when testing package p2p import ( "context" - "errors" "fmt" "sync" "time" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/notification" - "github.com/centrifuge/go-centrifuge/p2p/p2phandler" + "github.com/centrifuge/go-centrifuge/documents" + cented25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" "github.com/ipfs/go-cid" ds "github.com/ipfs/go-datastore" "github.com/ipfs/go-ipfs-addr" @@ -25,94 +24,91 @@ import ( ma "github.com/multiformats/go-multiaddr" mh "github.com/multiformats/go-multihash" "github.com/paralin/go-libp2p-grpc" - "golang.org/x/crypto/ed25519" ) -var log = logging.Logger("cent-p2p-server") -var HostInstance host.Host -var GRPCProtoInstance p2pgrpc.GRPCProtocol - -type CentP2PServer struct { - Port int - BootstrapPeers []string - PublicKey ed25519.PublicKey - PrivateKey ed25519.PrivateKey +var log = logging.Logger("p2p-server") + +// Config defines methods that are required for the package p2p. +type Config interface { + GetP2PExternalIP() string + GetP2PPort() int + GetBootstrapPeers() []string + GetP2PConnectionTimeout() time.Duration + GetNetworkID() uint32 + GetIdentityID() ([]byte, error) + GetSigningKeyPair() (pub, priv string) } -func NewCentP2PServer( - port int, - bootstrapPeers []string, - publicKey ed25519.PublicKey, - privateKey ed25519.PrivateKey, -) *CentP2PServer { - return &CentP2PServer{ - Port: port, - BootstrapPeers: bootstrapPeers, - PublicKey: publicKey, - PrivateKey: privateKey, - } +// p2pServer implements api.Server +type p2pServer struct { + config Config + host host.Host + registry *documents.ServiceRegistry + protocol *p2pgrpc.GRPCProtocol + handler p2ppb.P2PServiceServer } -func (*CentP2PServer) Name() string { - return "CentP2PServer" +// Name returns the P2PServer +func (*p2pServer) Name() string { + return "P2PServer" } -func (c *CentP2PServer) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) { +// Start starts the DHT and GRPC server for p2p communications +func (s *p2pServer) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) { defer wg.Done() - if c.Port == 0 { + if s.config.GetP2PPort() == 0 { startupErr <- errors.New("please provide a port to bind on") return } // Make a host that listens on the given multiaddress - hostInstance, err := c.makeBasicHost(c.Port) + var err error + s.host, err = s.makeBasicHost(s.config.GetP2PPort()) if err != nil { startupErr <- err return } - HostInstance = hostInstance + // Set the grpc protocol handler on it - grpcProto := p2pgrpc.NewGRPCProtocol(context.Background(), hostInstance) - GRPCProtoInstance = *grpcProto + s.protocol = p2pgrpc.NewGRPCProtocol(ctx, s.host) + p2ppb.RegisterP2PServiceServer(s.protocol.GetGRPCServer(), s.handler) - p2ppb.RegisterP2PServiceServer(grpcProto.GetGRPCServer(), &p2phandler.Handler{Notifier: ¬ification.WebhookSender{}}) - errOut := make(chan error) - go func(proto *p2pgrpc.GRPCProtocol, errOut chan<- error) { - errOut <- proto.Serve() - }(grpcProto, errOut) + serveErr := make(chan error) + go func() { + err := s.protocol.Serve() + serveErr <- err + }() - hostInstance.Peerstore().AddAddr(hostInstance.ID(), hostInstance.Addrs()[0], pstore.TempAddrTTL) + s.host.Peerstore().AddAddr(s.host.ID(), s.host.Addrs()[0], pstore.TempAddrTTL) // Start DHT - c.runDHT(ctx, hostInstance) - - for { - select { - case err := <-errOut: - log.Infof("failed to accept p2p grpc connections: %v\n", err) - startupErr <- err - return - case <-ctx.Done(): - log.Info("Shutting down GRPC server") - grpcProto.GetGRPCServer().Stop() - log.Info("GRPC server stopped") - return - } + s.runDHT(ctx, s.host) + select { + case err := <-serveErr: + log.Infof("GRPC server error: %v", err) + s.protocol.GetGRPCServer().GracefulStop() + log.Info("GRPC server stopped") + return + case <-ctx.Done(): + log.Info("Shutting down GRPC server") + s.protocol.GetGRPCServer().GracefulStop() + log.Info("GRPC server stopped") + return } + } -func (c *CentP2PServer) runDHT(ctx context.Context, h host.Host) error { +func (s *p2pServer) runDHT(ctx context.Context, h host.Host) error { + // Run it as a Bootstrap Node + dhtClient := dht.NewDHT(ctx, h, ds.NewMapDatastore()) - //dhtClient := dht.NewDHTClient(ctx, h, rdStore) // Just run it as a client, will not respond to discovery requests - dhtClient := dht.NewDHT(ctx, h, ds.NewMapDatastore()) // Run it as a Bootstrap Node + bootstrapPeers := s.config.GetBootstrapPeers() + log.Infof("Bootstrapping %s\n", bootstrapPeers) - log.Infof("Bootstrapping %s\n", c.BootstrapPeers) - for _, addr := range c.BootstrapPeers { + for _, addr := range bootstrapPeers { iaddr, _ := ipfsaddr.ParseString(addr) - pinfo, _ := pstore.InfoFromP2pAddr(iaddr.Multiaddr()) - if err := h.Connect(ctx, *pinfo); err != nil { log.Info("Bootstrapping to peer failed: ", err) } @@ -123,34 +119,37 @@ func (c *CentP2PServer) runDHT(ctx context.Context, h host.Host) error { // First, announce ourselves as participating in this topic log.Info("Announcing ourselves...") - tctx, _ := context.WithTimeout(ctx, time.Second*10) + tctx, cancel := context.WithTimeout(ctx, time.Second*10) if err := dhtClient.Provide(tctx, cidPref, true); err != nil { // Important to keep this as Non-Fatal error, otherwise it will fail for a node that behaves as well as bootstrap one log.Infof("Error: %s\n", err.Error()) } + cancel() // Now, look for others who have announced log.Info("Searching for other peers ...") - tctx, _ = context.WithTimeout(ctx, time.Second*10) + tctx, cancel = context.WithTimeout(ctx, time.Second*10) peers, err := dhtClient.FindProviders(tctx, cidPref) if err != nil { log.Error(err) } + cancel() log.Infof("Found %d peers!\n", len(peers)) - for _, p1 := range peers { - log.Infof("Peer %s %s\n", p1.ID.Pretty(), p1.Addrs) - } // Now connect to them, so they are added to the PeerStore for _, pe := range peers { + log.Infof("Peer %s %s\n", pe.ID.Pretty(), pe.Addrs) + if pe.ID == h.ID() { // No sense connecting to ourselves continue } - tctx, _ := context.WithTimeout(ctx, time.Second*5) + + tctx, cancel := context.WithTimeout(ctx, time.Second*5) if err := h.Connect(tctx, pe); err != nil { log.Info("Failed to connect to peer: ", err) } + cancel() } log.Info("Bootstrapping and discovery complete!") @@ -158,8 +157,8 @@ func (c *CentP2PServer) runDHT(ctx context.Context, h host.Host) error { } // makeBasicHost creates a LibP2P host with a peer ID listening on the given port -func (c *CentP2PServer) makeBasicHost(listenPort int) (host.Host, error) { - priv, pub, err := c.createSigningKey() +func (s *p2pServer) makeBasicHost(listenPort int) (host.Host, error) { + priv, pub, err := s.createSigningKey() if err != nil { return nil, err } @@ -184,20 +183,21 @@ func (c *CentP2PServer) makeBasicHost(listenPort int) (host.Host, error) { log.Infof("Could not enable encryption: %v\n", err) return nil, err } + err = ps.AddPrivKey(pid, priv) if err != nil { log.Infof("Could not enable encryption: %v\n", err) return nil, err } - externalIP := config.Config.GetP2PExternalIP() + externalIP := s.config.GetP2PExternalIP() var extMultiAddr ma.Multiaddr if externalIP == "" { log.Warning("External IP not defined, Peers might not be able to resolve this node if behind NAT\n") } else { extMultiAddr, err = ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", externalIP, listenPort)) if err != nil { - return nil, fmt.Errorf("failed to create multiaddr: %v", err) + return nil, errors.New("failed to create multiaddr: %v", err) } } @@ -223,39 +223,29 @@ func (c *CentP2PServer) makeBasicHost(listenPort int) (host.Host, error) { hostAddr, err := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", bhost.ID().Pretty())) if err != nil { - return nil, fmt.Errorf("failed to get addr: %v", err) + return nil, errors.New("failed to get addr: %v", err) } log.Infof("P2P Server at: %s %s\n", hostAddr.String(), bhost.Addrs()) return bhost, nil } -func (c *CentP2PServer) createSigningKey() (priv crypto.PrivKey, pub crypto.PubKey, err error) { +func (s *p2pServer) createSigningKey() (priv crypto.PrivKey, pub crypto.PubKey, err error) { // Create the signing key for the host + publicKey, privateKey, err := cented25519.GetSigningKeyPair(s.config.GetSigningKeyPair()) + if err != nil { + return nil, nil, errors.New("failed to get keys: %v", err) + } + var key []byte - key = append(key, c.PrivateKey...) - key = append(key, c.PublicKey...) + 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 } - -func GetHost() (h host.Host) { - h = HostInstance - if h == nil { - log.Fatal("Host undefined") - } - return -} - -func GetGRPCProto() (g *p2pgrpc.GRPCProtocol) { - g = &GRPCProtoInstance - if g == nil { - log.Fatal("Grpc not instantiated") - } - return -} diff --git a/p2p/server_test.go b/p2p/server_test.go index 0d32d67e4..698ea7bb0 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -9,11 +9,8 @@ import ( "testing" "time" - "github.com/centrifuge/go-centrifuge/config" - cented25519 "github.com/centrifuge/go-centrifuge/keytools/ed25519" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/assert" - "golang.org/x/crypto/ed25519" ) func TestCentP2PServer_Start(t *testing.T) { @@ -21,11 +18,10 @@ func TestCentP2PServer_Start(t *testing.T) { } func TestCentP2PServer_StartContextCancel(t *testing.T) { - priv, pub, err := getKeys() - assert.Nil(t, err) - cp2p := NewCentP2PServer(38203, []string{}, pub, priv) + cfg.Set("p2p.port", 38203) + cp2p := &p2pServer{config: cfg, handler: GRPCHandler(cfg, nil)} ctx, canc := context.WithCancel(context.Background()) - startErr := make(chan error) + startErr := make(chan error, 1) var wg sync.WaitGroup wg.Add(1) go cp2p.Start(ctx, &wg, startErr) @@ -33,19 +29,19 @@ func TestCentP2PServer_StartContextCancel(t *testing.T) { // cancel the context to shutdown the server canc() wg.Wait() + assert.Equal(t, 0, len(startErr), "should not error out") } func TestCentP2PServer_StartListenError(t *testing.T) { // cause an error by using an invalid port - priv, pub, err := getKeys() - assert.Nil(t, err) - cp2p := NewCentP2PServer(100000000, []string{}, pub, priv) + cfg.Set("p2p.port", 100000000) + cp2p := &p2pServer{config: cfg} ctx, _ := context.WithCancel(context.Background()) startErr := make(chan error) var wg sync.WaitGroup wg.Add(1) go cp2p.Start(ctx, &wg, startErr) - err = <-startErr + err := <-startErr wg.Wait() assert.NotNil(t, err, "Error should be not nil") assert.Equal(t, "failed to parse tcp: 100000000 failed to parse port addr: greater than 65536", err.Error()) @@ -53,9 +49,9 @@ func TestCentP2PServer_StartListenError(t *testing.T) { func TestCentP2PServer_makeBasicHostNoExternalIP(t *testing.T) { listenPort := 38202 - priv, pub, err := getKeys() - assert.Nil(t, err) - cp2p := NewCentP2PServer(listenPort, []string{}, pub, priv) + cfg.Set("p2p.port", listenPort) + cp2p := &p2pServer{config: cfg} + h, err := cp2p.makeBasicHost(listenPort) assert.Nil(t, err) assert.NotNil(t, h) @@ -64,10 +60,9 @@ func TestCentP2PServer_makeBasicHostNoExternalIP(t *testing.T) { func TestCentP2PServer_makeBasicHostWithExternalIP(t *testing.T) { externalIP := "100.100.100.100" listenPort := 38202 - config.Config.V.Set("p2p.externalIP", externalIP) - priv, pub, err := getKeys() - assert.Nil(t, err) - cp2p := NewCentP2PServer(listenPort, []string{}, pub, priv) + cfg.Set("p2p.port", listenPort) + cfg.Set("p2p.externalIP", externalIP) + cp2p := &p2pServer{config: cfg} h, err := cp2p.makeBasicHost(listenPort) assert.Nil(t, err) assert.NotNil(t, h) @@ -80,18 +75,10 @@ func TestCentP2PServer_makeBasicHostWithExternalIP(t *testing.T) { func TestCentP2PServer_makeBasicHostWithWrongExternalIP(t *testing.T) { externalIP := "100.200.300.400" listenPort := 38202 - config.Config.V.Set("p2p.externalIP", externalIP) - priv, pub, err := getKeys() - assert.Nil(t, err) - cp2p := NewCentP2PServer(listenPort, []string{}, pub, priv) + cfg.Set("p2p.port", listenPort) + cfg.Set("p2p.externalIP", externalIP) + cp2p := &p2pServer{config: cfg} h, err := cp2p.makeBasicHost(listenPort) assert.NotNil(t, err) assert.Nil(t, h) } - -func getKeys() (ed25519.PrivateKey, ed25519.PublicKey, error) { - pub, err := cented25519.GetPublicSigningKey("../build/resources/signingKey.pub.pem") - pri, err := cented25519.GetPrivateSigningKey("../build/resources/signingKey.key.pem") - return pri, pub, err - -} diff --git a/p2p/test_bootstrapper.go b/p2p/test_bootstrapper.go new file mode 100644 index 000000000..687eb2b56 --- /dev/null +++ b/p2p/test_bootstrapper.go @@ -0,0 +1,11 @@ +// +build unit integration + +package p2p + +func (b Bootstrapper) TestBootstrap(ctx map[string]interface{}) error { + return b.Bootstrap(ctx) +} + +func (b Bootstrapper) TestTearDown() error { + return nil +} diff --git a/p2p/p2phandler/validator.go b/p2p/validator.go similarity index 75% rename from p2p/p2phandler/validator.go rename to p2p/validator.go index 74fed95a0..4f1604425 100644 --- a/p2p/p2phandler/validator.go +++ b/p2p/validator.go @@ -1,4 +1,4 @@ -package p2phandler +package p2p import ( "fmt" @@ -6,11 +6,11 @@ import ( "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/config" - "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/errors" "github.com/centrifuge/go-centrifuge/version" ) +// Validator defines method that must be implemented by any validator type. type Validator interface { // Validate validates p2p requests Validate(header *p2ppb.CentrifugeHeader) error @@ -20,13 +20,13 @@ type Validator interface { type ValidatorGroup []Validator // Validate will execute all group specific atomic validations -func (group ValidatorGroup) Validate(header *p2ppb.CentrifugeHeader) (errors error) { +func (group ValidatorGroup) Validate(header *p2ppb.CentrifugeHeader) (errs error) { for _, v := range group { if err := v.Validate(header); err != nil { - errors = documents.AppendError(errors, err) + errs = errors.AppendError(errs, err) } } - return errors + return errs } // ValidatorFunc implements Validator and can be used as a adaptor for functions @@ -42,7 +42,7 @@ func (vf ValidatorFunc) Validate(header *p2ppb.CentrifugeHeader) error { func versionValidator() Validator { return ValidatorFunc(func(header *p2ppb.CentrifugeHeader) error { if header == nil { - return fmt.Errorf("nil header") + return errors.New("nil header") } if !version.CheckVersion(header.CentNodeVersion) { return version.IncompatibleVersionError(header.CentNodeVersion) @@ -51,22 +51,22 @@ func versionValidator() Validator { }) } -func networkValidator() Validator { +func networkValidator(networkID uint32) Validator { return ValidatorFunc(func(header *p2ppb.CentrifugeHeader) error { if header == nil { - return fmt.Errorf("nil header") + return errors.New("nil header") } - if config.Config.GetNetworkID() != header.NetworkIdentifier { - return incompatibleNetworkError(config.Config.GetNetworkID(), header.NetworkIdentifier) + if networkID != header.NetworkIdentifier { + return incompatibleNetworkError(networkID, header.NetworkIdentifier) } return nil }) } -func handshakeValidator() ValidatorGroup { +func handshakeValidator(networkID uint32) ValidatorGroup { return ValidatorGroup{ versionValidator(), - networkValidator(), + networkValidator(networkID), } } diff --git a/p2p/p2phandler/validator_test.go b/p2p/validator_test.go similarity index 85% rename from p2p/p2phandler/validator_test.go rename to p2p/validator_test.go index a6b139ed3..84289a802 100644 --- a/p2p/p2phandler/validator_test.go +++ b/p2p/validator_test.go @@ -1,12 +1,11 @@ // +build unit -package p2phandler +package p2p import ( "testing" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" - "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/version" "github.com/stretchr/testify/assert" ) @@ -40,7 +39,7 @@ func TestValidate_versionValidator(t *testing.T) { } func TestValidate_networkValidator(t *testing.T) { - nv := networkValidator() + nv := networkValidator(cfg.GetNetworkID()) // Nil header err := nv.Validate(nil) @@ -56,13 +55,13 @@ func TestValidate_networkValidator(t *testing.T) { assert.NotNil(t, err) // Compatible network - header.NetworkIdentifier = config.Config.GetNetworkID() + header.NetworkIdentifier = cfg.GetNetworkID() err = nv.Validate(header) assert.Nil(t, err) } func TestValidate_handshakeValidator(t *testing.T) { - hv := handshakeValidator() + hv := handshakeValidator(cfg.GetNetworkID()) // Incompatible version and network header := &p2ppb.CentrifugeHeader{ @@ -73,7 +72,7 @@ func TestValidate_handshakeValidator(t *testing.T) { assert.NotNil(t, err) // Incompatible version, correct network - header.NetworkIdentifier = config.Config.GetNetworkID() + header.NetworkIdentifier = cfg.GetNetworkID() err = hv.Validate(header) assert.NotNil(t, err) @@ -84,7 +83,7 @@ func TestValidate_handshakeValidator(t *testing.T) { assert.NotNil(t, err) // Compatible version and network - header.NetworkIdentifier = config.Config.GetNetworkID() + header.NetworkIdentifier = cfg.GetNetworkID() err = hv.Validate(header) assert.Nil(t, err) } diff --git a/protobufs/gen/go/nft/service.pb.go b/protobufs/gen/go/nft/service.pb.go index a52eabd2f..e16a63f6d 100644 --- a/protobufs/gen/go/nft/service.pb.go +++ b/protobufs/gen/go/nft/service.pb.go @@ -28,12 +28,10 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type NFTMintRequest struct { // Document identifier Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"` - // Document type - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // The contract address of the registry where the token should be minted - RegistryAddress string `protobuf:"bytes,3,opt,name=registry_address,json=registryAddress,proto3" json:"registry_address,omitempty"` - DepositAddress string `protobuf:"bytes,4,opt,name=deposit_address,json=depositAddress,proto3" json:"deposit_address,omitempty"` - ProofFields []string `protobuf:"bytes,5,rep,name=proof_fields,json=proofFields,proto3" json:"proof_fields,omitempty"` + RegistryAddress string `protobuf:"bytes,2,opt,name=registry_address,json=registryAddress,proto3" json:"registry_address,omitempty"` + DepositAddress string `protobuf:"bytes,3,opt,name=deposit_address,json=depositAddress,proto3" json:"deposit_address,omitempty"` + ProofFields []string `protobuf:"bytes,4,rep,name=proof_fields,json=proofFields,proto3" json:"proof_fields,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -43,7 +41,7 @@ func (m *NFTMintRequest) Reset() { *m = NFTMintRequest{} } func (m *NFTMintRequest) String() string { return proto.CompactTextString(m) } func (*NFTMintRequest) ProtoMessage() {} func (*NFTMintRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_service_deadd92188acecb1, []int{0} + return fileDescriptor_service_b4e252768b910acc, []int{0} } func (m *NFTMintRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_NFTMintRequest.Unmarshal(m, b) @@ -70,13 +68,6 @@ func (m *NFTMintRequest) GetIdentifier() string { return "" } -func (m *NFTMintRequest) GetType() string { - if m != nil { - return m.Type - } - return "" -} - func (m *NFTMintRequest) GetRegistryAddress() string { if m != nil { return m.RegistryAddress @@ -109,7 +100,7 @@ func (m *NFTMintResponse) Reset() { *m = NFTMintResponse{} } func (m *NFTMintResponse) String() string { return proto.CompactTextString(m) } func (*NFTMintResponse) ProtoMessage() {} func (*NFTMintResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_service_deadd92188acecb1, []int{1} + return fileDescriptor_service_b4e252768b910acc, []int{1} } func (m *NFTMintResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_NFTMintResponse.Unmarshal(m, b) @@ -213,32 +204,31 @@ var _NFTService_serviceDesc = grpc.ServiceDesc{ Metadata: "nft/service.proto", } -func init() { proto.RegisterFile("nft/service.proto", fileDescriptor_service_deadd92188acecb1) } - -var fileDescriptor_service_deadd92188acecb1 = []byte{ - // 371 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xc1, 0x8e, 0xd3, 0x30, - 0x10, 0x86, 0x95, 0xed, 0x2e, 0x65, 0xbd, 0xab, 0xed, 0x62, 0x38, 0x84, 0x08, 0x21, 0x93, 0x03, - 0x14, 0xd4, 0x36, 0x12, 0xdc, 0xb8, 0xb5, 0xa0, 0x48, 0x1c, 0x88, 0xaa, 0x92, 0x13, 0x97, 0x2a, - 0x4d, 0xc6, 0x91, 0x45, 0x33, 0x36, 0xf6, 0x94, 0xaa, 0x57, 0x24, 0x5e, 0x00, 0x1e, 0x87, 0xc7, - 0xe0, 0x15, 0x78, 0x10, 0x14, 0xa7, 0xad, 0xa8, 0xf6, 0x64, 0xfb, 0x9b, 0xcf, 0xa3, 0x5f, 0x33, - 0xec, 0x01, 0x4a, 0x4a, 0x1c, 0xd8, 0x6f, 0xaa, 0x84, 0x89, 0xb1, 0x9a, 0x34, 0xef, 0xa1, 0xa4, - 0xe8, 0x49, 0xad, 0x75, 0xbd, 0x86, 0xa4, 0x30, 0x2a, 0x29, 0x10, 0x35, 0x15, 0xa4, 0x34, 0xba, - 0x4e, 0x89, 0x46, 0xfe, 0x28, 0xc7, 0x35, 0xe0, 0xd8, 0x6d, 0x8b, 0xba, 0x06, 0x9b, 0x68, 0xe3, - 0x8d, 0xbb, 0x76, 0xfc, 0x3b, 0x60, 0x37, 0x59, 0x9a, 0x7f, 0x54, 0x48, 0x0b, 0xf8, 0xba, 0x01, - 0x47, 0xfc, 0x29, 0x63, 0xaa, 0x02, 0x24, 0x25, 0x15, 0xd8, 0x30, 0x10, 0xc1, 0xf0, 0x72, 0xf1, - 0x1f, 0xe1, 0x9c, 0x9d, 0xd3, 0xce, 0x40, 0x78, 0xe6, 0x2b, 0xfe, 0xce, 0x5f, 0xb2, 0x5b, 0x0b, - 0xb5, 0x72, 0x64, 0x77, 0xcb, 0xa2, 0xaa, 0x2c, 0x38, 0x17, 0xf6, 0x7c, 0x7d, 0x70, 0xe0, 0xd3, - 0x0e, 0xf3, 0x17, 0x6c, 0x50, 0x81, 0xd1, 0x4e, 0xd1, 0xd1, 0x3c, 0xf7, 0xe6, 0xcd, 0x1e, 0x1f, - 0xc4, 0x67, 0xec, 0xda, 0x58, 0xad, 0xe5, 0x52, 0x2a, 0x58, 0x57, 0x2e, 0xbc, 0x10, 0xbd, 0xe1, - 0xe5, 0xe2, 0xca, 0xb3, 0xd4, 0xa3, 0x78, 0xc4, 0x06, 0xc7, 0xf0, 0xce, 0x68, 0x74, 0xc0, 0x1f, - 0xb3, 0xfb, 0xa4, 0xbf, 0x00, 0x2e, 0x55, 0xb5, 0xcf, 0xde, 0xf7, 0xef, 0x0f, 0xd5, 0xeb, 0x1f, - 0x01, 0x63, 0x59, 0x9a, 0x7f, 0xea, 0x26, 0xca, 0xb7, 0xac, 0xdf, 0xfe, 0xcc, 0xd2, 0x9c, 0x3f, - 0x9c, 0xa0, 0xa4, 0xc9, 0xe9, 0x1c, 0xa2, 0x47, 0xa7, 0xb0, 0xeb, 0x1f, 0x4f, 0x7f, 0x4e, 0x87, - 0xd1, 0xf3, 0x16, 0x89, 0x02, 0x45, 0x96, 0xe6, 0x42, 0x5a, 0xdd, 0x88, 0x42, 0xbc, 0x03, 0x24, - 0xab, 0xe4, 0xa6, 0x06, 0xf1, 0x5e, 0x97, 0x9b, 0x06, 0x90, 0xbe, 0xff, 0xf9, 0xfb, 0xeb, 0xec, - 0x36, 0xbe, 0x4a, 0x7c, 0x82, 0xa4, 0x51, 0x48, 0x6f, 0x83, 0x57, 0x33, 0xc1, 0xfa, 0xa5, 0x6e, - 0xda, 0xee, 0xb3, 0xeb, 0x7d, 0x98, 0x79, 0xbb, 0x8c, 0x79, 0xf0, 0xf9, 0x02, 0x25, 0x99, 0xd5, - 0xea, 0x9e, 0x5f, 0xce, 0x9b, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x69, 0xb7, 0x27, 0x68, 0x02, - 0x02, 0x00, 0x00, +func init() { proto.RegisterFile("nft/service.proto", fileDescriptor_service_b4e252768b910acc) } + +var fileDescriptor_service_b4e252768b910acc = []byte{ + // 361 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0x41, 0x6e, 0xda, 0x40, + 0x14, 0x86, 0x65, 0x68, 0x4b, 0x19, 0x10, 0x50, 0xb7, 0x0b, 0x6a, 0x55, 0xd5, 0xd4, 0x8b, 0x96, + 0x56, 0x80, 0xa5, 0x76, 0xd7, 0x1d, 0xb4, 0xb2, 0xd4, 0x45, 0x2d, 0x44, 0xbd, 0xea, 0x06, 0x19, + 0xfb, 0x8d, 0x35, 0x0a, 0x7e, 0xe3, 0xcc, 0x3c, 0x82, 0xb2, 0x8d, 0x94, 0x0b, 0x24, 0x87, 0xc8, + 0x81, 0x72, 0x85, 0x1c, 0x24, 0xf2, 0x18, 0x50, 0x50, 0x56, 0xa3, 0xf9, 0xf4, 0xcd, 0xd3, 0x3f, + 0xff, 0x63, 0x6f, 0x50, 0x50, 0x60, 0x40, 0x5f, 0xc8, 0x14, 0xa6, 0xa5, 0x56, 0xa4, 0xdc, 0x26, + 0x0a, 0xf2, 0x3e, 0xe4, 0x4a, 0xe5, 0x1b, 0x08, 0x92, 0x52, 0x06, 0x09, 0xa2, 0xa2, 0x84, 0xa4, + 0x42, 0x53, 0x2b, 0xde, 0xd8, 0x1e, 0xe9, 0x24, 0x07, 0x9c, 0x98, 0x5d, 0x92, 0xe7, 0xa0, 0x03, + 0x55, 0x5a, 0xe3, 0xb9, 0xed, 0xdf, 0x39, 0xac, 0x17, 0x85, 0xf1, 0x5f, 0x89, 0xb4, 0x84, 0xf3, + 0x2d, 0x18, 0x72, 0x3f, 0x32, 0x26, 0x33, 0x40, 0x92, 0x42, 0x82, 0x1e, 0x3a, 0xdc, 0x19, 0xb5, + 0x97, 0x4f, 0x88, 0xfb, 0x95, 0x0d, 0x34, 0xe4, 0xd2, 0x90, 0xbe, 0x5c, 0x25, 0x59, 0xa6, 0xc1, + 0x98, 0x61, 0xc3, 0x5a, 0xfd, 0x03, 0x9f, 0xd5, 0xd8, 0xfd, 0xc2, 0xfa, 0x19, 0x94, 0xca, 0x48, + 0x3a, 0x9a, 0x4d, 0x6b, 0xf6, 0xf6, 0xf8, 0x20, 0x7e, 0x62, 0xdd, 0x52, 0x2b, 0x25, 0x56, 0x42, + 0xc2, 0x26, 0x33, 0xc3, 0x17, 0xbc, 0x39, 0x6a, 0x2f, 0x3b, 0x96, 0x85, 0x16, 0xf9, 0x63, 0xd6, + 0x3f, 0x06, 0x35, 0xa5, 0x42, 0x03, 0xee, 0x7b, 0xf6, 0x9a, 0xd4, 0x19, 0xe0, 0x4a, 0x66, 0xfb, + 0x9c, 0x2d, 0x7b, 0xff, 0x93, 0x7d, 0xbf, 0x76, 0x18, 0x8b, 0xc2, 0xf8, 0x5f, 0xdd, 0x9e, 0xbb, + 0x63, 0xad, 0xea, 0x65, 0x14, 0xc6, 0xee, 0xdb, 0x29, 0x0a, 0x9a, 0x9e, 0xfe, 0xd9, 0x7b, 0x77, + 0x0a, 0xeb, 0xf9, 0xfe, 0xec, 0x66, 0x36, 0xf2, 0x3e, 0x57, 0x88, 0x27, 0xc8, 0xa3, 0x30, 0xe6, + 0x42, 0xab, 0x82, 0x27, 0xfc, 0x17, 0x20, 0x69, 0x29, 0xb6, 0x39, 0xf0, 0xdf, 0x2a, 0xdd, 0x16, + 0x80, 0x74, 0x75, 0xff, 0x70, 0xdb, 0x18, 0xf8, 0x9d, 0xc0, 0x26, 0x08, 0x0a, 0x89, 0xf4, 0xd3, + 0xf9, 0x36, 0xe7, 0xac, 0x95, 0xaa, 0xa2, 0x9a, 0x3e, 0xef, 0xee, 0xc3, 0x2c, 0xaa, 0xe2, 0x17, + 0xce, 0xff, 0x97, 0x28, 0xa8, 0x5c, 0xaf, 0x5f, 0xd9, 0x45, 0xfc, 0x78, 0x0c, 0x00, 0x00, 0xff, + 0xff, 0x82, 0x2c, 0xea, 0x0e, 0xee, 0x01, 0x00, 0x00, } diff --git a/protobufs/gen/swagger.json b/protobufs/gen/swagger.json index c687f2417..9241fdf10 100644 --- a/protobufs/gen/swagger.json +++ b/protobufs/gen/swagger.json @@ -1 +1 @@ -{"swagger":"2.0","info":{"version":"0.0.1","title":"Centrifuge OS Node API","description":"\n","contact":{"name":"Centrifuge","url":"https://github.com/centrifuge/go-centrifuge","email":"hello@centrifuge.io"}},"host":"localhost","basePath":"","schemes":["https"],"consumes":["application/json"],"produces":["application/json"],"tags":[],"definitions":{"documentCreateDocumentProofForVersionRequest":{"type":"object","properties":{"identifier":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}}},"documentCreateDocumentProofRequest":{"type":"object","properties":{"identifier":{"type":"string"},"type":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}}},"documentDocumentProof":{"type":"object","properties":{"header":{"$ref":"#/definitions/documentResponseHeader"},"field_proofs":{"type":"array","items":{"$ref":"#/definitions/documentProof"}}}},"documentProof":{"type":"object","properties":{"property":{"type":"string"},"value":{"type":"string"},"salt":{"type":"string"},"hash":{"type":"string","title":"hash is filled if value & salt are not available"},"sorted_hashes":{"type":"array","items":{"type":"string"}}}},"documentResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"}},"title":"ResponseHeader contains a set of common fields for most documents"},"healthPong":{"type":"object","properties":{"version":{"type":"string"},"network":{"type":"string"}},"title":"Pong contains basic information about the node"},"invoiceInvoiceCreatePayload":{"type":"object","properties":{"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceInvoiceData":{"type":"object","properties":{"invoice_status":{"type":"string"},"invoice_number":{"type":"string","title":"invoice number or reference number"},"sender_name":{"type":"string","title":"name of the sender company"},"sender_street":{"type":"string","title":"street and address details of the sender company"},"sender_city":{"type":"string"},"sender_zipcode":{"type":"string"},"sender_country":{"type":"string","title":"country ISO code of the sender of this invoice"},"recipient_name":{"type":"string","title":"name of the recipient company"},"recipient_street":{"type":"string"},"recipient_city":{"type":"string"},"recipient_zipcode":{"type":"string"},"recipient_country":{"type":"string","title":"country ISO code of the receipient of this invoice"},"currency":{"type":"string","title":"ISO currency code"},"gross_amount":{"type":"string","format":"int64","title":"invoice amount including tax"},"net_amount":{"type":"string","format":"int64","title":"invoice amount excluding tax"},"tax_amount":{"type":"string","format":"int64"},"tax_rate":{"type":"string","format":"int64"},"recipient":{"type":"string"},"sender":{"type":"string"},"payee":{"type":"string"},"comment":{"type":"string"},"due_date":{"type":"string","format":"date-time"},"date_created":{"type":"string","format":"date-time"},"extra_data":{"type":"string"}}},"invoiceInvoiceResponse":{"type":"object","properties":{"header":{"$ref":"#/definitions/invoiceResponseHeader"},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceInvoiceUpdatePayload":{"type":"object","properties":{"identifier":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}}},"title":"ResponseHeader contains a set of common fields for most documents"},"nftNFTMintRequest":{"type":"object","properties":{"identifier":{"type":"string","title":"Document identifier"},"type":{"type":"string","title":"Document type"},"registry_address":{"type":"string","title":"The contract address of the registry where the token should be minted"},"deposit_address":{"type":"string"},"proof_fields":{"type":"array","items":{"type":"string"}}}},"nftNFTMintResponse":{"type":"object","properties":{"token_id":{"type":"string"}}},"notificationNotificationMessage":{"type":"object","properties":{"event_type":{"type":"integer","format":"int64"},"centrifuge_id":{"type":"string","format":"byte"},"recorded":{"type":"string","format":"date-time"},"document_type":{"type":"string"},"document_identifier":{"type":"string","format":"byte"}},"title":"NotificationMessage wraps a single CoreDocument to be notified to upstream services"},"purchaseorderPurchaseOrderCreatePayload":{"type":"object","properties":{"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderPurchaseOrderData":{"type":"object","properties":{"po_status":{"type":"string"},"po_number":{"type":"string","title":"purchase order number or reference number"},"order_name":{"type":"string","title":"name of the ordering company"},"order_street":{"type":"string","title":"street and address details of the ordering company"},"order_city":{"type":"string"},"order_zipcode":{"type":"string"},"order_country":{"type":"string","title":"country ISO code of the ordering company of this purchase order"},"recipient_name":{"type":"string","title":"name of the recipient company"},"recipient_street":{"type":"string"},"recipient_city":{"type":"string"},"recipient_zipcode":{"type":"string"},"recipient_country":{"type":"string","title":"country ISO code of the receipient of this purchase order"},"currency":{"type":"string","title":"ISO currency code"},"order_amount":{"type":"string","format":"int64","title":"ordering gross amount including tax"},"net_amount":{"type":"string","format":"int64","title":"invoice amount excluding tax"},"tax_amount":{"type":"string","format":"int64"},"tax_rate":{"type":"string","format":"int64"},"recipient":{"type":"string"},"order":{"type":"string"},"order_contact":{"type":"string","title":"contact or requester or purchaser at the ordering company"},"comment":{"type":"string"},"delivery_date":{"type":"string","format":"date-time","title":"requested delivery date"},"date_created":{"type":"string","format":"date-time","title":"purchase order date"},"extra_data":{"type":"string"}}},"purchaseorderPurchaseOrderResponse":{"type":"object","properties":{"header":{"$ref":"#/definitions/purchaseorderResponseHeader"},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderPurchaseOrderUpdatePayload":{"type":"object","properties":{"identifier":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}}},"title":"ResponseHeader contains a set of common fields for most documents"}},"paths":{"/document/{identifier}/proof":{"post":{"description":"Creates a list of precise proofs for the specified fields of the document given by ID","operationId":"CreateDocumentProof","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/documentDocumentProof"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/documentCreateDocumentProofRequest"}}],"tags":["DocumentService"]}},"/document/{identifier}/{version}/proof":{"post":{"description":"Creates a list of precise proofs for the specified fields of the given version of the document given by ID","operationId":"CreateDocumentProofForVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/documentDocumentProof"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/documentCreateDocumentProofForVersionRequest"}}],"tags":["DocumentService"]}},"/ping":{"get":{"description":"Health check for the Node","operationId":"Ping","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/healthPong"}}},"tags":["HealthCheckService"]}},"/invoice":{"post":{"description":"Creates an invoice","operationId":"Create","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/invoiceInvoiceCreatePayload"}}],"tags":["DocumentService"]}},"/invoice/{identifier}":{"get":{"description":"Get the current invoice","operationId":"Get","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]},"put":{"description":"Updates an invoice","operationId":"Update","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/invoiceInvoiceUpdatePayload"}}],"tags":["DocumentService"]}},"/invoice/{identifier}/{version}":{"get":{"description":"Get a specific version of an invoice","operationId":"GetVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]}},"/token/mint":{"post":{"description":"Mint an NFT from a Centrifuge Document","operationId":"MintNFT","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/nftNFTMintResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/nftNFTMintRequest"}}],"tags":["NFTService"]}},"/dummy":{"get":{"description":"Dummy notification endpoint","operationId":"Notify","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/notificationNotificationMessage"}}},"tags":["NotificationDummyService"]}},"/purchaseorder":{"post":{"description":"Creates a purchase order","operationId":"Create","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderCreatePayload"}}],"tags":["DocumentService"]}},"/purchaseorder/{identifier}":{"get":{"description":"Get the current version of a purchase order","operationId":"Get","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]},"put":{"description":"Updates a purchase order","operationId":"Update","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderUpdatePayload"}}],"tags":["DocumentService"]}},"/purchaseorder/{identifier}/{version}":{"get":{"description":"Get a specific version of a purchase order","operationId":"GetVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]}}}} \ No newline at end of file +{"swagger":"2.0","info":{"version":"0.0.2","title":"Centrifuge OS Node API","description":"\n","contact":{"name":"Centrifuge","url":"https://github.com/centrifuge/go-centrifuge","email":"hello@centrifuge.io"}},"host":"localhost","basePath":"","schemes":["https"],"consumes":["application/json"],"produces":["application/json"],"tags":[],"definitions":{"documentCreateDocumentProofForVersionRequest":{"type":"object","properties":{"identifier":{"type":"string"},"type":{"type":"string"},"version":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}}},"documentCreateDocumentProofRequest":{"type":"object","properties":{"identifier":{"type":"string"},"type":{"type":"string"},"fields":{"type":"array","items":{"type":"string"}}}},"documentDocumentProof":{"type":"object","properties":{"header":{"$ref":"#/definitions/documentResponseHeader"},"field_proofs":{"type":"array","items":{"$ref":"#/definitions/documentProof"}}}},"documentProof":{"type":"object","properties":{"property":{"type":"string"},"value":{"type":"string"},"salt":{"type":"string"},"hash":{"type":"string","title":"hash is filled if value & salt are not available"},"sorted_hashes":{"type":"array","items":{"type":"string"}}}},"documentResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"}},"title":"ResponseHeader contains a set of common fields for most documents"},"healthPong":{"type":"object","properties":{"version":{"type":"string"},"network":{"type":"string"}},"title":"Pong contains basic information about the node"},"invoiceInvoiceCreatePayload":{"type":"object","properties":{"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceInvoiceData":{"type":"object","properties":{"invoice_status":{"type":"string"},"invoice_number":{"type":"string","title":"invoice number or reference number"},"sender_name":{"type":"string","title":"name of the sender company"},"sender_street":{"type":"string","title":"street and address details of the sender company"},"sender_city":{"type":"string"},"sender_zipcode":{"type":"string"},"sender_country":{"type":"string","title":"country ISO code of the sender of this invoice"},"recipient_name":{"type":"string","title":"name of the recipient company"},"recipient_street":{"type":"string"},"recipient_city":{"type":"string"},"recipient_zipcode":{"type":"string"},"recipient_country":{"type":"string","title":"country ISO code of the receipient of this invoice"},"currency":{"type":"string","title":"ISO currency code"},"gross_amount":{"type":"string","format":"int64","title":"invoice amount including tax"},"net_amount":{"type":"string","format":"int64","title":"invoice amount excluding tax"},"tax_amount":{"type":"string","format":"int64"},"tax_rate":{"type":"string","format":"int64"},"recipient":{"type":"string"},"sender":{"type":"string"},"payee":{"type":"string"},"comment":{"type":"string"},"due_date":{"type":"string","format":"date-time"},"date_created":{"type":"string","format":"date-time"},"extra_data":{"type":"string"}}},"invoiceInvoiceResponse":{"type":"object","properties":{"header":{"$ref":"#/definitions/invoiceResponseHeader"},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceInvoiceUpdatePayload":{"type":"object","properties":{"identifier":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/invoiceInvoiceData"}}},"invoiceResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}}},"title":"ResponseHeader contains a set of common fields for most documents"},"nftNFTMintRequest":{"type":"object","properties":{"identifier":{"type":"string","title":"Document identifier"},"registry_address":{"type":"string","title":"The contract address of the registry where the token should be minted"},"deposit_address":{"type":"string"},"proof_fields":{"type":"array","items":{"type":"string"}}}},"nftNFTMintResponse":{"type":"object","properties":{"token_id":{"type":"string"}}},"notificationNotificationMessage":{"type":"object","properties":{"event_type":{"type":"integer","format":"int64"},"centrifuge_id":{"type":"string"},"recorded":{"type":"string","format":"date-time"},"document_type":{"type":"string"},"document_id":{"type":"string"}},"title":"NotificationMessage wraps a single CoreDocument to be notified to upstream services"},"purchaseorderPurchaseOrderCreatePayload":{"type":"object","properties":{"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderPurchaseOrderData":{"type":"object","properties":{"po_status":{"type":"string"},"po_number":{"type":"string","title":"purchase order number or reference number"},"order_name":{"type":"string","title":"name of the ordering company"},"order_street":{"type":"string","title":"street and address details of the ordering company"},"order_city":{"type":"string"},"order_zipcode":{"type":"string"},"order_country":{"type":"string","title":"country ISO code of the ordering company of this purchase order"},"recipient_name":{"type":"string","title":"name of the recipient company"},"recipient_street":{"type":"string"},"recipient_city":{"type":"string"},"recipient_zipcode":{"type":"string"},"recipient_country":{"type":"string","title":"country ISO code of the receipient of this purchase order"},"currency":{"type":"string","title":"ISO currency code"},"order_amount":{"type":"string","format":"int64","title":"ordering gross amount including tax"},"net_amount":{"type":"string","format":"int64","title":"invoice amount excluding tax"},"tax_amount":{"type":"string","format":"int64"},"tax_rate":{"type":"string","format":"int64"},"recipient":{"type":"string"},"order":{"type":"string"},"order_contact":{"type":"string","title":"contact or requester or purchaser at the ordering company"},"comment":{"type":"string"},"delivery_date":{"type":"string","format":"date-time","title":"requested delivery date"},"date_created":{"type":"string","format":"date-time","title":"purchase order date"},"extra_data":{"type":"string"}}},"purchaseorderPurchaseOrderResponse":{"type":"object","properties":{"header":{"$ref":"#/definitions/purchaseorderResponseHeader"},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderPurchaseOrderUpdatePayload":{"type":"object","properties":{"identifier":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}},"data":{"$ref":"#/definitions/purchaseorderPurchaseOrderData"}}},"purchaseorderResponseHeader":{"type":"object","properties":{"document_id":{"type":"string"},"version_id":{"type":"string"},"state":{"type":"string"},"collaborators":{"type":"array","items":{"type":"string"}}},"title":"ResponseHeader contains a set of common fields for most documents"}},"paths":{"/document/{identifier}/proof":{"post":{"description":"Creates a list of precise proofs for the specified fields of the document given by ID","operationId":"CreateDocumentProof","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/documentDocumentProof"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/documentCreateDocumentProofRequest"}}],"tags":["DocumentService"]}},"/document/{identifier}/{version}/proof":{"post":{"description":"Creates a list of precise proofs for the specified fields of the given version of the document given by ID","operationId":"CreateDocumentProofForVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/documentDocumentProof"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/documentCreateDocumentProofForVersionRequest"}}],"tags":["DocumentService"]}},"/ping":{"get":{"description":"Health check for the Node","operationId":"Ping","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/healthPong"}}},"tags":["HealthCheckService"]}},"/invoice":{"post":{"description":"Creates an invoice","operationId":"Create","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/invoiceInvoiceCreatePayload"}}],"tags":["DocumentService"]}},"/invoice/{identifier}":{"get":{"description":"Get the current invoice","operationId":"Get","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]},"put":{"description":"Updates an invoice","operationId":"Update","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/invoiceInvoiceUpdatePayload"}}],"tags":["DocumentService"]}},"/invoice/{identifier}/{version}":{"get":{"description":"Get a specific version of an invoice","operationId":"GetVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/invoiceInvoiceResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]}},"/token/mint":{"post":{"description":"Mint an NFT from a Centrifuge Document","operationId":"MintNFT","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/nftNFTMintResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/nftNFTMintRequest"}}],"tags":["NFTService"]}},"/dummy":{"get":{"description":"Dummy notification endpoint","operationId":"Notify","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/notificationNotificationMessage"}}},"tags":["NotificationDummyService"]}},"/purchaseorder":{"post":{"description":"Creates a purchase order","operationId":"Create","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderCreatePayload"}}],"tags":["DocumentService"]}},"/purchaseorder/{identifier}":{"get":{"description":"Get the current version of a purchase order","operationId":"Get","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]},"put":{"description":"Updates a purchase order","operationId":"Update","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderUpdatePayload"}}],"tags":["DocumentService"]}},"/purchaseorder/{identifier}/{version}":{"get":{"description":"Get a specific version of a purchase order","operationId":"GetVersion","responses":{"200":{"description":"","schema":{"$ref":"#/definitions/purchaseorderPurchaseOrderResponse"}}},"parameters":[{"name":"identifier","in":"path","required":true,"type":"string"},{"name":"version","in":"path","required":true,"type":"string"}],"tags":["DocumentService"]}}}} \ No newline at end of file diff --git a/protobufs/gen/swagger/nft/service.swagger.json b/protobufs/gen/swagger/nft/service.swagger.json index 7667df0ae..31792eb39 100644 --- a/protobufs/gen/swagger/nft/service.swagger.json +++ b/protobufs/gen/swagger/nft/service.swagger.json @@ -51,10 +51,6 @@ "type": "string", "title": "Document identifier" }, - "type": { - "type": "string", - "title": "Document type" - }, "registry_address": { "type": "string", "title": "The contract address of the registry where the token should be minted" diff --git a/protobufs/gen/swagger/notification/service.swagger.json b/protobufs/gen/swagger/notification/service.swagger.json index 1a68285af..8d1e1f753 100644 --- a/protobufs/gen/swagger/notification/service.swagger.json +++ b/protobufs/gen/swagger/notification/service.swagger.json @@ -42,8 +42,7 @@ "format": "int64" }, "centrifuge_id": { - "type": "string", - "format": "byte" + "type": "string" }, "recorded": { "type": "string", @@ -52,9 +51,8 @@ "document_type": { "type": "string" }, - "document_identifier": { - "type": "string", - "format": "byte" + "document_id": { + "type": "string" } }, "title": "NotificationMessage wraps a single CoreDocument to be notified to upstream services" diff --git a/protobufs/nft/service.proto b/protobufs/nft/service.proto index 00d900f14..83cd2e0c2 100644 --- a/protobufs/nft/service.proto +++ b/protobufs/nft/service.proto @@ -26,12 +26,10 @@ service NFTService { message NFTMintRequest { // Document identifier string identifier = 1; - // Document type - string type = 2; // The contract address of the registry where the token should be minted - string registry_address = 3; - string deposit_address = 4; - repeated string proof_fields = 5; + string registry_address = 2; + string deposit_address = 3; + repeated string proof_fields = 4; } message NFTMintResponse { diff --git a/queue/bootstrapper.go b/queue/bootstrapper.go index 4c0e873a7..b2acc20de 100644 --- a/queue/bootstrapper.go +++ b/queue/bootstrapper.go @@ -1,35 +1,25 @@ package queue -import "errors" +import ( + "github.com/centrifuge/go-centrifuge/errors" -const BootstrappedQueuedTasks string = "BootstrappedQueuedTasks" + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/config" +) +// Bootstrapper implements bootstrap.Bootstrapper. type Bootstrapper struct { + context map[string]interface{} } -func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { - // to see how BootstrappedQueuedTasks get populated check usages of InstallQueuedTask - if queuedTasks, ok := context[BootstrappedQueuedTasks]; ok { - if queuedTasksTyped, ok := queuedTasks.([]QueuedTask); ok { - InitQueue(queuedTasksTyped) - return nil - } - } - return errors.New("could not find the list of " + BootstrappedQueuedTasks) -} - -// InstallQueuedTask adds a queued task to the context so that when the queue initializes it can update it self -// with different tasks types queued in the node -func InstallQueuedTask(context map[string]interface{}, queuedTask QueuedTask) error { - if queuedTasks, ok := context[BootstrappedQueuedTasks]; ok { - if queuedTasksTyped, ok := queuedTasks.([]QueuedTask); ok { - context[BootstrappedQueuedTasks] = append(queuedTasksTyped, queuedTask) - return nil - } else { - return errors.New(BootstrappedQueuedTasks + " is of an unexpected type") - } - } else { - context[BootstrappedQueuedTasks] = []QueuedTask{queuedTask} - return nil +// Bootstrap initiates the queue. +func (b *Bootstrapper) Bootstrap(context map[string]interface{}) error { + if _, ok := context[bootstrap.BootstrappedConfig]; !ok { + return errors.New("config hasn't been initialized") } + cfg := context[bootstrap.BootstrappedConfig].(config.Configuration) + srv := &Server{config: cfg, taskTypes: []TaskType{}} + context[bootstrap.BootstrappedQueueServer] = srv + b.context = context + return nil } diff --git a/queue/bootstrapper_test.go b/queue/bootstrapper_test.go deleted file mode 100644 index b7c1dbc06..000000000 --- a/queue/bootstrapper_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// +build unit - -package queue - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -type MockQueuedTask struct{} - -func (MockQueuedTask) Name() string { - return "MockTask" -} - -func (MockQueuedTask) Init() error { - return nil -} - -func TestInstallQueuedTaskCreatingNewListOfTasks(t *testing.T) { - context := map[string]interface{}{} - err := InstallQueuedTask(context, MockQueuedTask{}) - assert.Nil(t, err, "Installation of tasks should be successful") - assert.Equal(t, 1, len(context[BootstrappedQueuedTasks].([]QueuedTask))) -} - -func TestInstallQueuedTaskappendingToListOfTasks(t *testing.T) { - queuedTasks := []QueuedTask{MockQueuedTask{}} - context := map[string]interface{}{BootstrappedQueuedTasks: queuedTasks} - err := InstallQueuedTask(context, MockQueuedTask{}) - assert.Nil(t, err, "Installation of tasks should be successful") - assert.Equal(t, 2, len(context[BootstrappedQueuedTasks].([]QueuedTask))) -} - -func TestInstallQueuedTaskQueuedTasksListHasInvalidType(t *testing.T) { - err := InstallQueuedTask(map[string]interface{}{BootstrappedQueuedTasks: 1}, MockQueuedTask{}) - assert.NotNil(t, err, "Installation of tasks should NOT be successful") -} diff --git a/queue/queue.go b/queue/queue.go deleted file mode 100644 index 4f3b73d2e..000000000 --- a/queue/queue.go +++ /dev/null @@ -1,40 +0,0 @@ -package queue - -import ( - "sync" - - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/gocelery" -) - -var Queue *gocelery.CeleryClient -var queueInit sync.Once - -// QueuedTask is a task to be queued in the centrifuge node to be completed asynchronously -type QueuedTask interface { - Name() string - Init() error -} - -func InitQueue(tasks []QueuedTask) { - queueInit.Do(func() { - var err error - Queue, err = gocelery.NewCeleryClient( - gocelery.NewInMemoryBroker(), - gocelery.NewInMemoryBackend(), - config.Config.GetNumWorkers(), - config.Config.GetWorkerWaitTimeMS(), - ) - if err != nil { - panic("Could not initialize the queue") - } - for _, task := range tasks { - task.Init() - } - Queue.StartWorker() - }) -} - -func StopQueue() { - Queue.StopWorker() -} diff --git a/queue/server.go b/queue/server.go new file mode 100644 index 000000000..ff4e95755 --- /dev/null +++ b/queue/server.go @@ -0,0 +1,122 @@ +package queue + +import ( + "context" + "sync" + "time" + + "github.com/centrifuge/go-centrifuge/errors" + + "github.com/centrifuge/gocelery" + logging "github.com/ipfs/go-log" +) + +// Constants are commonly used by all the tasks through kwargs. +const ( + BlockHeightParam string = "BlockHeight" + TimeoutParam string = "Timeout" +) + +var log = logging.Logger("queue-server") + +// Config is an interface for queue specific configurations +type Config interface { + + // GetNumWorkers gets the number of background workers to initiate + GetNumWorkers() int + + // GetWorkerWaitTime gets the worker wait time for a task to be available while polling + // increasing this may slow down task execution while reducing it may consume a lot of CPU cycles + GetWorkerWaitTimeMS() int +} + +// TaskType is a task to be queued in the centrifuge node to be completed asynchronously +type TaskType interface { + + // TaskTypeName of the task + TaskTypeName() string +} + +// TaskResult represents a result from a queued task execution +type TaskResult interface { + + // Get the result within a timeout from the queue task execution + Get(timeout time.Duration) (interface{}, error) +} + +// Server represents the queue server currently implemented based on gocelery +type Server struct { + config Config + lock sync.RWMutex + queue *gocelery.CeleryClient + taskTypes []TaskType +} + +// Name of the queue server +func (qs *Server) Name() string { + return "QueueServer" +} + +// Start the queue server +func (qs *Server) Start(ctx context.Context, wg *sync.WaitGroup, startupErr chan<- error) { + defer wg.Done() + qs.lock.Lock() + var err error + qs.queue, err = gocelery.NewCeleryClient( + gocelery.NewInMemoryBroker(), + gocelery.NewInMemoryBackend(), + qs.config.GetNumWorkers(), + qs.config.GetWorkerWaitTimeMS(), + ) + if err != nil { + startupErr <- err + } + for _, task := range qs.taskTypes { + qs.queue.Register(task.TaskTypeName(), task) + } + // start the workers + qs.queue.StartWorker() + qs.lock.Unlock() + + <-ctx.Done() + log.Info("Shutting down Queue server with context done") + qs.lock.Lock() + qs.queue.StopWorker() + qs.lock.Unlock() + log.Info("Queue server stopped") +} + +// RegisterTaskType registers a task type on the queue server +func (qs *Server) RegisterTaskType(name string, task interface{}) { + qs.taskTypes = append(qs.taskTypes, task.(TaskType)) +} + +// EnqueueJob enqueues a job on the queue server for the given taskTypeName +func (qs *Server) EnqueueJob(taskTypeName string, params map[string]interface{}) (TaskResult, error) { + qs.lock.RLock() + defer qs.lock.RUnlock() + if qs.queue == nil { + return nil, errors.New("queue hasn't been initialised") + } + return qs.queue.DelayKwargs(taskTypeName, params) +} + +// GetDuration parses key parameter to time.Duration type +func GetDuration(key interface{}) (time.Duration, error) { + f64, ok := key.(float64) + if !ok { + return time.Duration(0), errors.New("Could not parse interface to float64") + } + return time.Duration(f64), nil +} + +// ParseBlockHeight parses blockHeight interface param to uint64 +func ParseBlockHeight(valMap map[string]interface{}) (uint64, error) { + if bhi, ok := valMap[BlockHeightParam]; ok { + bhf, ok := bhi.(float64) + if ok { + return uint64(bhf), nil + } + } + return 0, errors.New("value can not be parsed") +} diff --git a/queue/test_bootstrapper.go b/queue/test_bootstrapper.go index fab8ade08..b36bad946 100644 --- a/queue/test_bootstrapper.go +++ b/queue/test_bootstrapper.go @@ -2,11 +2,38 @@ package queue +import ( + "context" + "sync" + + "github.com/centrifuge/go-centrifuge/bootstrap" +) + func (b *Bootstrapper) TestBootstrap(context map[string]interface{}) error { return b.Bootstrap(context) } func (b *Bootstrapper) TestTearDown() error { - StopQueue() + return nil +} + +type Starter struct { + canF context.CancelFunc +} + +func (s *Starter) TestBootstrap(ctx map[string]interface{}) error { + // handle the special case for running the queue server after task types have been registered (done by node bootstrapper at runtime) + qs := ctx[bootstrap.BootstrappedQueueServer].(*Server) + childErr := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + c, canF := context.WithCancel(context.Background()) + s.canF = canF + go qs.Start(c, &wg, childErr) + return nil +} + +func (s *Starter) TestTearDown() error { + s.canF() return nil } diff --git a/resources/data.go b/resources/data.go index 80111fde7..f3f7d5b5f 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\x8c\x56\xc9\x76\xdb\x3a\x0f\xde\xeb\x29\x70\x9c\xcd\xff\x2f\x62\x6b\xb0\x64\x5b\xbb\x8c\x9d\x9c\x5c\xc7\x71\x9b\x26\x3b\x8a\x84\x2c\xd6\x12\xa9\x90\x94\x87\x3e\xfd\x3d\xa4\xa4\x34\x43\xd3\x5e\xaf\x78\x0c\xe0\xc3\xf4\x01\xd0\x11\x9c\x63\x4e\x9a\xd2\x00\xc3\x2d\x96\xb2\xae\x50\x18\x30\xa8\x8d\x40\x03\x64\x4d\xb8\xd0\x06\x14\x17\x1b\xcc\x0e\x1e\x45\x61\x14\xcf\x9b\x35\x5e\xa3\xd9\x49\xb5\x49\x41\x35\x5a\x73\x22\x0a\x5e\x96\x9e\x03\xe3\x02\xc1\x14\x08\xac\xc3\x15\xad\xa6\x06\x53\x10\x03\x67\x4f\x08\x50\x11\x2e\x8c\xc5\xf7\x7a\x95\xd4\x03\x38\x82\xb9\xa4\xa4\x74\x21\x70\xb1\x06\x2a\x85\x51\x84\x1a\x20\x8c\x29\xd4\x1a\x35\x08\x44\x06\x46\x42\x86\xa0\xd1\xc0\x8e\x9b\x02\x50\x6c\x61\x4b\x14\x27\x59\x89\x7a\xe8\x41\x6f\x6f\x21\x01\x38\x4b\x21\x8a\x22\xf7\x46\x53\xa0\xc2\xa6\xea\x32\xf8\xc4\x52\x98\x46\xd3\x56\x96\x49\x69\xb4\x51\xa4\x5e\x20\x2a\xdd\xda\x02\x1c\xc3\x60\xc4\xeb\xf1\x28\x08\x27\x43\x7f\xe8\x0f\x83\x91\xa1\xf5\x28\x9a\x86\x7e\x38\xe2\x75\xae\x47\x37\xd5\xea\x66\x9f\xed\x36\xcd\xc3\xfd\xfd\x79\xde\xfc\x5c\x65\xfb\x8b\x93\x25\xae\xae\xcf\xe6\xf2\xe7\xe1\x10\xc7\xd3\xed\x8d\x58\x7f\xdb\x2e\xae\x7e\xcc\xef\x37\x83\xbf\xc2\x46\x3d\xec\xb7\x3c\xb9\xb8\x4e\xaa\xcd\xe3\x1d\xfe\xb8\xfb\x72\x17\x3e\x2e\x9a\x20\xf9\x5e\xb3\x0f\xd1\xe6\xb3\x0c\x56\x51\x55\x90\x62\x71\x1a\xdf\x62\x2c\x82\x16\xb6\x2f\xd7\x49\x5f\xad\x3e\x09\xce\x50\x18\x6e\x0e\x97\x84\x1a\xa9\x0e\x29\x0c\x06\xaf\x24\x4b\x5c\x73\x6d\x5e\x88\x88\xa0\x85\x54\x4b\xac\xa5\xe6\xaf\xac\x6a\x72\xb0\x54\xf9\x27\x2b\xf9\x9a\x18\x2e\x85\x93\xb9\x06\x5e\x11\x2e\x7e\x4b\xa7\xae\xcf\x1e\x3c\x67\x4d\x1b\xe0\x11\x5c\x37\x15\x2a\x4e\xe1\xd3\x39\xc8\xdc\x31\xe8\x19\x57\x7e\x59\xb6\xcd\x8c\x83\xce\xea\xb4\xef\x18\x94\x5c\x1b\x6b\x29\x24\xc3\xb7\x64\xab\x95\xdc\x72\x27\x90\x0e\xfb\x59\x00\x7d\x78\xff\x81\x01\x51\x3c\x0c\xc3\x78\x18\xfa\xfe\x70\x1c\xbe\x66\x41\x10\x9e\x47\x5f\xa4\xbc\x9b\x73\x4e\x6f\xbe\xed\x56\xc5\xea\xf4\x3e\xd9\x7f\xa1\x0b\x39\xcf\x93\xe5\xcd\xfd\xe7\xcb\x7a\x97\x07\x6a\x12\xef\xe6\xfb\xf0\x61\x19\xd5\x67\x2c\x78\xcd\x85\xce\xc1\x34\x19\x86\x81\xff\x9e\x83\x9b\x87\xab\x93\xe9\x87\xc5\x47\xb5\xbd\x78\x38\x9d\xed\xd8\x46\x7e\xa5\x27\x27\xd5\xd9\xc3\xc7\x7a\x86\x87\xc3\xc3\xf8\xf6\x62\xba\xbe\x54\x51\xb1\xba\xfe\x3e\xe8\xea\x74\xd1\xb1\xbe\xaf\xa4\x2d\xf3\x31\x2c\xbb\xb9\x7e\x67\x2e\xc6\x9d\xf1\x9c\xd8\x12\x01\xc3\xba\x94\x07\x64\x70\x5b\x11\x65\xe0\xac\xa3\x9a\x86\x5c\x2a\x57\xd4\x35\xdf\xa2\x78\x51\xce\xb7\x74\x84\x77\xf9\xe8\xef\x67\x3e\x0b\x67\xe3\x78\x12\xe0\x24\x9a\x8e\xc3\x64\x36\x21\x49\x92\x4d\xc8\x6c\x46\xfc\x19\x63\x09\x9d\x44\x2c\x8a\x13\xf6\x07\xe6\xfa\xfb\x59\x92\xf8\xd4\x8f\x66\x2c\x0a\x82\x71\x1c\x91\xdc\x67\xf1\x94\xc6\x49\x92\x4c\xc2\x88\xcd\x68\x98\x93\x09\x4b\x90\xfe\x81\xe3\xfe\x7e\x92\x4f\xe3\x31\xcb\xc9\x6c\xea\x07\x21\x9b\xe4\x24\x8e\xe9\xd4\x8f\xb2\x8c\x84\x61\xe2\x67\x94\x21\x8e\xb3\x18\xd9\x5f\xa6\xe1\x08\x2c\x8d\x8e\x8d\x3c\xae\x11\x95\xad\x46\xce\xd7\x8d\x72\x1a\xda\xab\xc3\xba\xdd\x78\x2b\x5e\xa1\x6c\x0c\xec\x0a\x14\x20\x6b\x14\xdd\xe2\x13\x48\x9d\xa6\x25\xad\x05\xd0\x1e\xf4\x7f\x77\x26\x29\x0c\x22\x5f\x3b\x4f\x37\x0d\x36\xf8\xca\x85\xeb\x0c\xd1\x07\x41\x0b\x25\x85\x6c\xb4\x9d\x03\x8a\x5a\x73\xb1\xf6\x1e\xad\x41\x1b\x40\xbb\xb6\xb5\x6b\xa2\x68\xaa\x0c\x95\x9d\x24\x4b\x03\x54\x7a\x44\xa5\xd0\x76\x38\xbb\xa9\xda\xd9\x99\xc9\x10\x48\x59\x4a\x4a\x0c\x32\x20\x06\xb4\x21\xca\x34\xb5\x07\xd6\xfe\xae\x35\x4c\x21\x74\xe8\x97\x0a\x51\x43\x53\xc3\xd9\xe2\x2b\xd0\x03\x2d\x51\xb7\xa9\xb6\x0e\x80\x6b\xd8\x11\xee\xb6\xbd\x8d\x17\xb7\x28\x8c\x4d\xb5\x15\xdf\x11\xee\xb2\xbd\xba\x4d\x21\xb0\x89\x3e\x51\x59\xd7\x48\x79\xce\xe9\xcb\xa4\xbd\x9e\xc8\x6d\x6a\xb7\x58\xa2\xe5\xe8\xae\xe0\xb4\x78\x22\x39\x10\x4a\x65\x63\x57\x93\x84\x46\x63\xbf\x71\xa4\x2d\x42\xb7\x2a\x18\x70\xe1\xfe\xa4\x8d\x36\xb2\xea\x9c\x40\xce\x4b\xf4\xa0\xbf\x6e\x27\x2d\xcc\x35\xa9\x30\x85\x81\xbd\x68\x83\xa7\x1b\x66\x83\xe9\x81\x9f\xfc\xd2\x92\xdb\x8d\x68\x97\x14\xfc\x6f\x87\xa0\xf0\xb1\xe1\x0a\x61\xa7\x41\x2a\xe0\x35\xed\x0e\x9b\xbd\x63\xf6\x49\x89\xb1\x61\xbb\x92\xfc\xdf\x56\x57\x32\xfc\xba\x9c\xa7\xb0\xd3\xe9\x68\x64\x1b\x50\x16\x52\x9b\x74\x16\x8f\x93\xbe\x95\xee\xec\xae\x89\xcd\x84\x53\x1b\xec\x9a\xe8\x85\x7d\xa6\x10\xf8\xfd\xef\x8d\x72\xc9\x2b\x6e\x5a\xe5\xb9\x7d\xa6\x30\x9e\x04\x61\x34\x9d\xbe\xa0\xa8\x91\xae\x57\x2d\xb1\xc4\xaf\xbc\x8c\x22\x42\x13\x47\xd7\x3e\x03\xc6\xda\x33\x4d\x20\x2b\x25\xdd\x00\x11\xac\x4b\x04\x8c\xe2\xeb\x35\x2a\x64\x2d\xa1\x0d\xee\x4d\xdf\xe6\x96\xd4\x89\x6f\x59\xfd\x9e\x63\x85\x84\x81\x14\xe5\xc1\x0e\x4b\x4f\xf5\xfe\x5b\xa5\x0f\xe9\x17\xf4\x12\x09\x7b\x09\x1f\xc4\x1d\xfa\xb5\xed\xc3\xf3\xd8\x6b\x29\x4b\xa8\xc8\x1e\x14\x1a\xc5\xdb\x7b\xa1\x51\x30\x20\x2f\xd4\xe4\x16\x95\x07\x56\x71\xd9\xea\xa5\x10\x76\x35\xfd\x3d\x24\x17\x06\xd5\x96\x94\x0e\xf7\xd0\xd2\x9f\xd8\x00\x69\xa3\x94\xbb\x91\xcf\x2c\x0a\xa2\x21\x43\xb4\x47\xd4\x20\x35\xae\x4c\x3d\x80\xf5\x67\x77\x54\xd8\x65\x70\xce\xb5\xe3\x8a\x43\xd4\xb2\x7a\xc3\x35\x0d\x4c\x82\x90\x06\x74\x53\xd7\x52\x19\x30\x7b\x17\x11\xa9\xb9\xfd\x4c\xda\x2f\xa4\x2c\x4f\xa8\x5d\x0a\x17\xc2\x22\xb1\x14\x8c\x6a\xd0\xf3\xfe\x0d\x00\x00\xff\xff\x0a\x16\x18\x68\x1b\x0a\x00\x00") +var _goCentrifugeBuildConfigsDefault_configYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\x59\x73\xdb\xba\x15\x7e\xd7\xaf\x38\x23\xbf\xb4\x33\xa5\xcc\x7d\xd1\xcc\x9d\x8e\xbc\x25\xb9\x71\x5c\xd9\x96\xe3\x1b\xbf\x34\x20\x70\x28\x22\xa6\x00\x06\x00\xb5\xe4\xd7\x77\x00\x52\x8e\x1d\x2f\xb7\xcb\x34\x2f\xa6\x00\x9c\xfd\x3b\x1f\x0e\x72\x00\x27\x58\x91\xae\x31\xc0\x70\x8d\x8d\x6c\x57\x28\x0c\x18\xd4\x46\xa0\x01\xb2\x24\x5c\x68\x03\x8a\x8b\x7b\x2c\x77\x23\x8a\xc2\x28\x5e\x75\x4b\xbc\x40\xb3\x91\xea\x7e\x0a\xaa\xd3\x9a\x13\x51\xf3\xa6\x19\x39\x65\x5c\x20\x98\x1a\x81\x0d\x7a\x45\x7f\x52\x83\xa9\x89\x81\xe3\x07\x0d\xb0\x22\x5c\x18\xab\x7f\xb4\x3f\x32\x1d\x01\x1c\xc0\xb9\xa4\xa4\x71\x2e\x70\xb1\x04\x2a\x85\x51\x84\x1a\x20\x8c\x29\xd4\x1a\x35\x08\x44\x06\x46\x42\x89\xa0\xd1\xc0\x86\x9b\x1a\x50\xac\x61\x4d\x14\x27\x65\x83\x7a\x32\x82\xbd\xbc\x55\x09\xc0\xd9\x14\xa2\x28\x72\xdf\x68\x6a\x54\xd8\xad\x86\x08\x3e\xb0\x29\xe4\x51\xde\xef\x95\x52\x1a\x6d\x14\x69\xe7\x88\x4a\xf7\xb2\x1e\x8c\x0f\x79\x1b\x1f\x06\x61\x36\xf1\x27\xfe\x24\x38\x34\xb4\x3d\x8c\xf2\xd0\x0f\x0f\x79\x5b\xe9\xc3\xcb\xd5\xe2\x72\x5b\x6e\xee\xbb\xbb\x2f\x5f\x4e\xaa\xee\xc7\xa2\xdc\x9e\xce\xae\x70\x71\x71\x7c\x2e\x7f\xec\x76\x49\x92\xaf\x2f\xc5\xf2\xf3\x7a\xfe\xe9\xdb\xf9\x97\xfb\xf1\x9f\x28\x8d\xf6\x4a\x3f\x57\xe9\xe9\x45\xba\xba\xff\x7e\x8b\xdf\x6e\x3f\xde\x86\xdf\xe7\x5d\x90\xfe\xd1\xb2\x77\xd1\xfd\xef\x32\x58\x44\xab\x9a\xd4\xf3\xa3\xe4\x1a\x13\x11\xf4\x4a\xf7\xa9\x9a\xed\x33\xd5\x07\x60\xc3\x47\x61\xb8\xd9\x9d\x11\x6a\xa4\xda\x4d\x61\x3c\xfe\x65\xe7\x0a\x97\x5c\x9b\x27\x5b\x44\xd0\x5a\xaa\x2b\x6c\xa5\xe6\xbf\x48\xb5\x64\x67\x61\xf2\x8f\xb2\xe1\x4b\x62\xb8\x14\x6e\xcf\x15\xef\x13\xe1\xe2\x45\x28\x0d\x35\x1e\xc1\x63\xc4\xf4\x0e\x1e\xc0\x45\xb7\x42\xc5\x29\x7c\x38\x01\x59\x39\xf4\x3c\xc2\xc9\x4f\xc9\xbe\x90\x49\x30\x48\x1d\xed\xab\x05\x0d\xd7\xc6\x4a\x0a\xc9\xf0\x39\xd0\x5a\x25\xd7\xdc\x6d\x48\xa7\xfb\x91\x03\x7b\xf7\xfe\xb4\xfa\x51\x32\x09\xc3\x64\x12\xfa\xfe\x24\x0e\x7f\x45\x40\x10\x9e\x44\x1f\xa5\xbc\x3d\xe7\x9c\x5e\x7e\xde\x2c\xea\xc5\xd1\x97\x74\xfb\x91\xce\xe5\x79\x95\x5e\x5d\x7e\xf9\xfd\xac\xdd\x54\x81\xca\x92\xcd\xf9\x36\xbc\xbb\x8a\xda\x63\x16\x8c\x5f\x52\x9f\xa7\x93\x30\xf0\x5f\x53\x7f\x79\xf7\x69\x96\xbf\x9b\xbf\x57\xeb\xd3\xbb\xa3\x62\xc3\xee\xe5\x0d\x9d\xcd\x56\xc7\x77\xef\xdb\x02\x77\xbb\xbb\xf8\xfa\x34\x5f\x9e\xa9\xa8\x5e\x5c\xfc\x31\x1e\x72\x74\x3a\xa0\x7d\x9f\x45\x9b\x62\x0f\xae\x86\x7e\x7e\xa5\x1f\xe2\x41\xf8\x9c\xd8\xf4\x00\xc3\xb6\x91\x3b\x64\x70\xbd\x22\xca\xc0\xf1\x00\x33\x0d\x95\x54\x2e\xa1\x4b\xbe\x46\xf1\x24\x95\xcf\xa1\x08\xaf\x62\xd1\xdf\x16\x3e\x0b\x8b\x38\xc9\x02\xcc\xa2\x3c\x0e\xd3\x22\x23\x69\x5a\x66\xa4\x28\x88\x5f\x30\x96\xd2\x2c\x62\x51\x92\xb2\x37\x50\xeb\x6f\x8b\x34\xf5\xa9\x1f\x15\x2c\x0a\x82\x38\x89\x48\xe5\xb3\x24\xa7\x49\x9a\xa6\x59\x18\xb1\x82\x86\x15\xc9\x58\x8a\xf4\x0d\x7c\xfb\xdb\xac\xca\x93\x98\x55\xa4\xc8\xfd\x20\x64\x59\x45\x92\x84\xe6\x7e\x54\x96\x24\x0c\x53\xbf\xa4\x0c\x31\x2e\x13\x64\x6f\x75\x82\xbf\x65\xa5\x9f\xe4\xc1\xac\x88\xc2\x3c\x4d\xe3\x3c\x49\xa2\x30\x9f\xb1\x93\xd2\x3f\x0d\x93\x20\xc8\xe3\x34\xf6\xab\x02\x93\x13\xd7\x33\x25\x2a\x41\x9a\x1a\xf9\xb2\x36\x03\xe8\x0e\x0e\x0e\x86\x0a\x7c\x94\x6b\x22\xe0\x6c\x76\x39\xfc\xf6\xe0\xd6\xb2\x1d\x17\x55\xa7\x08\xec\x64\x07\x4b\x4b\xd3\x02\x50\x29\xa9\x2c\x9c\x16\x35\xd7\xa0\xf0\x7b\x67\x2b\xc7\x35\x08\x69\x40\x77\x6d\x2b\x95\x41\x06\x25\x52\xd2\x69\xb4\x92\xca\x75\x8b\x3d\xa2\x3a\x21\x2c\xd5\x3a\x22\xd5\x86\x18\xdb\x32\x9d\x5d\x9a\xc0\x55\x27\xfa\x75\xcf\x1b\xd6\x7e\x23\x8a\xd6\x7c\x8d\x93\xf1\xdf\x06\xa7\x00\x36\xb6\xe3\x8c\x04\x26\xff\xee\x24\x08\x34\x8e\xc4\x5b\xa2\xb8\xd9\xf5\x86\x9c\x96\x7b\x17\x0f\x2e\xa7\xfd\xcf\xaf\xc3\x01\xcf\xa3\x35\xe1\xe2\xb7\x7e\xdb\xf3\xac\xb7\xbf\x45\x7e\xe4\xc7\xe0\x79\x1b\xa2\xda\xe1\x8f\x57\x12\xa5\x38\x2a\x48\xd2\xdc\xf7\x7d\x1f\x3c\x4f\x48\x8f\x08\xca\x51\x18\xaf\x6c\x24\xbd\xd7\xfd\x9a\x46\xb5\x46\xaf\xb1\x49\x05\xcf\x5b\x91\xad\xd7\xda\xa6\x86\x30\xb1\x42\x5a\x90\x56\xd7\xd2\x0c\x8b\x6e\x6d\xc5\xc5\x93\x9f\xd6\x67\x42\x0d\x5f\x23\x78\x9e\x05\xb3\x4d\x91\xac\xaa\xe7\x99\x00\xcf\x63\xa5\x47\xe5\xaa\xb5\xe7\xa5\x00\xad\x99\x0d\x89\xd0\x1a\x3d\xcd\x7f\x20\xc4\x7e\x91\x82\xe7\x7d\xd3\x52\xa8\x96\x7a\xb5\xd4\x46\x03\x69\x9a\x47\x6b\x5c\x18\x54\x15\xa1\x68\xd7\xbf\x3e\x2d\xf7\xf3\x64\xbe\x54\xf9\x23\x1b\x3e\x32\xdb\x7b\x02\x7b\x47\x8c\x84\x5b\x2c\xaf\xed\xba\xd1\xe0\x72\xa2\xa0\x52\x72\x05\x9d\x30\xaa\xd3\x16\x12\x52\xf1\x25\x17\x53\x98\x4c\xc6\xaf\xd6\xd3\x36\xf9\xb3\x5a\x7e\xf5\xbc\x4e\x68\x52\xa1\x87\xdb\x56\x6a\xfc\x0a\x55\x43\x96\xbf\x00\xf8\x3f\x63\xf6\xf0\x7f\x64\xf6\x27\xbd\xf4\x6f\x73\x7b\xe0\xc7\x93\x20\x89\x27\x41\x3e\x49\x9e\xdd\xee\x7b\xf2\x9d\xeb\x94\x13\xbc\xe9\xce\xee\x2e\xba\xe0\xdd\x76\xad\x77\x47\x8b\x6b\xb5\xd0\xc5\xda\x1c\xa5\xa5\xf9\x34\x13\xef\xcf\xe4\xf9\xb7\xf2\xfe\xc7\x31\x19\xbf\xa0\x3e\x99\x04\x79\x32\x09\xa3\xec\x55\x03\xc7\xef\xe8\x86\x2f\xbe\xc9\x8f\xb7\xef\xab\x23\x12\xe7\xe1\xcd\xdc\x10\xbc\xd9\x5e\x9c\x6f\x58\xfe\xa3\x14\x47\xc1\x75\xb6\xc1\xd9\xdd\xcd\xf6\xee\x6d\x76\x77\xa4\xf1\x2a\xb7\x87\xff\x07\x72\x7f\x83\xdb\xf3\xa4\x8c\xc2\x2a\x23\x51\x15\xfb\x71\x1e\x54\x41\x18\x45\xb1\x1f\x07\x69\xe6\xd3\x9c\x96\xe8\x67\x55\xc6\xb2\x82\xbe\xc9\xed\x49\x4c\x30\xca\xa2\xca\x2f\xd2\x8a\x54\x21\x2b\xd3\x32\x27\x71\x9a\x05\x19\xf5\xcb\x22\x47\x5a\x11\x3f\x4b\x18\x7b\x93\xdb\xe3\x38\xae\xd2\xb8\xc0\xc8\xcf\xe2\x38\xc4\x2c\xa5\xb4\xca\xa2\x2c\x4e\x53\x4c\xc2\x2a\x48\xfd\xa2\x2c\xf2\x30\xf5\xdf\xe6\x76\x3f\x0e\x32\x2c\xa3\xac\x88\x83\x20\x8d\xa3\x34\x8f\xfd\xe0\x24\x4d\xd3\x22\x8f\xe9\xe9\x49\x96\x16\xf1\xec\x88\x1e\x95\xc1\x78\x64\xc7\x61\x62\x08\x5c\x1b\xa9\xc8\x12\x47\xba\xff\xdb\x0f\xb9\x73\x62\x6a\x97\xe2\xc6\xce\x4a\x27\x47\x50\xf1\x06\x47\xd6\xa8\xa9\xa7\x70\x68\x56\xed\xe1\xcf\x61\xfb\x9f\x8c\x18\x32\x71\x27\x59\x69\xf5\x1e\x4b\x51\xf1\x65\xa7\x9c\x5b\x0f\x06\xa8\x5b\xbd\xfe\xef\xcd\xf4\x0a\x9e\x59\xfb\xf0\x40\x4d\x1b\x0b\x27\x87\x8a\xd9\xfc\x03\x10\xc1\x60\x1e\xce\xe1\xba\xe7\x15\xdb\xaa\x28\x6c\x2f\x8e\x6c\xb7\xbe\x97\xda\x08\xb2\xc2\x29\xf8\x6e\xc4\xf5\x47\x07\x30\x97\xca\x0c\x4a\xac\x82\x97\x05\xed\xa1\x29\xe4\x7e\x1e\x5a\xe3\xb6\x5f\x3d\x23\x1d\x35\x03\x7d\x1c\xb7\x1e\xb5\x61\xdb\x87\x79\xdd\x22\xe5\xd5\x0e\x4e\xb7\xc6\x31\x00\x7c\x98\x3f\xf2\xd5\x51\x16\x25\xc2\x3e\x18\x14\x5a\x56\x66\x40\x0c\xf0\x0a\x4a\xac\xb9\x60\x70\x31\x5b\x58\x35\x38\x48\x7f\x98\x4f\x61\x33\xd9\x4e\x76\x93\x1f\x7d\x12\xad\xd7\x9d\x46\xf6\xd0\x13\x36\xea\x86\xec\x50\xd9\x54\x3a\x77\x5d\x43\xbb\xd3\x0b\xbe\x42\xd9\xb9\x30\x05\xc8\x16\xc5\xf0\x8a\x19\x38\xd9\x71\x95\xbb\x67\x46\xb0\x5f\x1e\x44\xa6\x30\x8e\x7c\xed\x80\x73\xd9\x61\x87\xbf\x84\xeb\xac\x13\xbd\x13\xb4\x56\x52\xc8\x4e\x5b\xfa\xa3\xa8\x35\x17\xcb\xd1\x77\x2b\xd0\x27\xa3\x7f\x83\xe9\x3e\xf4\x6e\x55\xa2\xb2\x04\x6a\xfb\x1f\x95\x3e\xa4\x52\x68\xcb\xc9\x03\x99\x6e\xec\x10\x5c\xba\x4b\x47\x52\x62\xfa\xcc\x68\x43\x94\xe9\xda\x11\x58\xf9\xdb\x5e\x70\x0a\x7d\x78\x67\x0a\x51\x43\xd7\xc2\xf1\xfc\x06\xe8\x8e\x36\xa8\xfb\x50\x7b\x03\x76\x9e\xd8\x10\xee\x9e\x6e\xd6\x5f\x5c\xa3\x30\x36\xd4\x7e\xfb\x96\x70\x17\xed\xa7\xeb\x29\x04\x36\xd0\x07\x06\xd3\xae\x84\x9c\x3e\x0d\x7a\xb4\x67\xb0\xa1\xce\xd8\xa0\xe5\xa6\x4d\xcd\x69\xfd\xc0\x6e\x40\x28\x95\x9d\x70\x37\x96\x1d\x6e\x86\x8b\x46\xda\x24\x0c\x37\x04\x03\xde\xdf\x62\xb4\xd3\x46\xae\x06\x23\xfb\x6e\x18\x9e\xaa\xb3\x5e\xcd\x85\x03\xed\xd8\x3e\x4f\xc7\x0f\x0f\x52\xd7\x68\x83\xe2\x07\xbb\xb4\xb1\x73\x47\x8f\xaf\xbf\x6c\xd0\x8d\x5d\x5c\x21\x6c\x34\x48\x05\xbc\xa5\xc3\x2b\xd5\x3e\x4a\xed\x27\x25\xc6\xba\xed\x52\xf2\x57\x9b\x5d\xc9\xf0\xe6\xea\x7c\x0a\xb5\x31\xed\xf4\xf0\xd0\xdd\xf3\x76\x38\x98\x16\x49\x9c\xec\x8b\xe9\x5e\xd1\x4b\x62\x63\xe1\xd4\xba\xbb\x24\x7a\x6e\x3f\xa7\x10\xf8\xfb\x7f\xcf\x0e\x37\x7c\xc5\x4d\x7f\xf8\xdc\x7e\x4e\x21\xce\x82\x30\xca\xf3\x27\x20\x35\xd2\x55\xab\x87\x96\xf8\x19\x99\x51\x44\x68\xf2\x30\x44\xd8\x18\x18\xeb\x5f\xdd\x04\xdc\x9c\xe5\xba\xbf\x0f\x05\x8c\xe2\xcb\x25\x2a\x64\x3d\xa4\x0d\x6e\xcd\xbe\xd0\x3d\xac\x53\xdf\xe2\xfa\x35\xc3\x0a\x09\x03\x29\x9a\x9d\x6d\x97\x3d\xd8\xf7\xff\xf5\xb0\x77\xe9\xa7\xea\x2b\x24\xec\xa9\xfa\x20\x19\xb4\x5f\xd8\x4a\x3c\xf6\xbd\x95\xb2\x81\x15\xd9\x82\x42\xa3\x78\x3f\x28\x68\x14\x0c\xc8\x93\x63\x72\xed\x5a\x79\x45\xb6\x57\xfd\xb9\x29\x84\x43\x4e\x5f\x56\xe9\xa6\xb5\x35\x69\x9c\xde\x5d\xdf\x00\xc4\x3a\x48\x3b\xa5\xdc\xb3\xf7\x91\x44\x4d\x34\x94\x88\xf6\x5d\x6c\x90\x1a\x97\xa6\xbd\x02\x6b\xcf\x5e\x4f\xe1\x10\xc1\x09\xd7\x0e\x2d\x4e\xa3\x96\xab\x67\x68\xd3\xc0\xe4\xe3\xa1\x1e\xcc\xd6\x79\x44\x5a\x3e\x02\x30\xdb\xb9\x94\xcd\x8c\x5a\x5a\x38\x15\x56\x13\x9b\x82\x51\x1d\xda\x5e\x23\x62\x07\x0c\xcb\x6e\xb9\x1c\x28\xc9\xb6\x80\x23\x80\xa5\x04\x6b\x64\xe4\x76\xfb\x56\x6b\x5b\x25\x2b\x57\x9e\x07\x11\x4b\x76\x76\x75\x0a\x15\x69\x34\x8e\xfe\x15\x00\x00\xff\xff\x54\x43\x46\xfe\x3c\x12\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: 2587, mode: os.FileMode(420), modTime: time.Unix(1540892585, 0)} + info := bindataFileInfo{name: "go-centrifuge/build/configs/default_config.yaml", size: 4668, mode: os.FileMode(420), modTime: time.Unix(1544540466, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/signatures/signatures.go b/signatures/signatures.go index 3248a8c21..ed927c48b 100644 --- a/signatures/signatures.go +++ b/signatures/signatures.go @@ -2,50 +2,18 @@ package signatures import ( "errors" - "fmt" "time" "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/identity" "github.com/centrifuge/go-centrifuge/utils" "golang.org/x/crypto/ed25519" ) -func ValidateCentrifugeID(signature *coredocumentpb.Signature, centrifugeId identity.CentID) error { - centIDSignature, err := identity.ToCentID(signature.EntityId) - if err != nil { - return err - } - - if !centrifugeId.Equal(centIDSignature) { - return errors.New("signature entity doesn't match provided centID") - } - - return nil - -} - -// ValidateSignature verifies the signature on the document -func ValidateSignature(signature *coredocumentpb.Signature, message []byte) error { - centID, err := identity.ToCentID(signature.EntityId) - if err != nil { - return err - } - - err = identity.ValidateKey(centID, signature.PublicKey, identity.KeyPurposeSigning) - if err != nil { - return err - } - - return verifySignature(signature.PublicKey, message, signature.Signature) -} - -// verifySignature verifies the signature using ed25519 -func verifySignature(pubKey, message, signature []byte) error { +// VerifySignature verifies the signature using ed25519 +func VerifySignature(pubKey, message, signature []byte) error { valid := ed25519.Verify(pubKey, message, signature) if !valid { - return fmt.Errorf("invalid signature") + return errors.New("invalid signature") } return nil @@ -53,11 +21,11 @@ func verifySignature(pubKey, message, signature []byte) error { // Sign the document with the private key and return the signature along with the public key for the verification // assumes that signing root for the document is generated -func Sign(idConfig *config.IdentityConfig, payload []byte) *coredocumentpb.Signature { +func Sign(centIDBytes []byte, privateKey []byte, pubKey []byte, payload []byte) *coredocumentpb.Signature { return &coredocumentpb.Signature{ - EntityId: idConfig.ID, - PublicKey: idConfig.PublicKey, - Signature: ed25519.Sign(idConfig.PrivateKey, payload), + EntityId: centIDBytes, + PublicKey: pubKey, + Signature: ed25519.Sign(privateKey, payload), Timestamp: utils.ToTimestamp(time.Now().UTC()), } } diff --git a/signatures/signatures_test.go b/signatures/signatures_test.go index 43efe70e9..5054e034c 100644 --- a/signatures/signatures_test.go +++ b/signatures/signatures_test.go @@ -3,14 +3,8 @@ package signatures import ( - "fmt" "testing" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/testingutils" - "github.com/centrifuge/go-centrifuge/testingutils/commons" "github.com/centrifuge/go-centrifuge/utils" "github.com/stretchr/testify/assert" ) @@ -23,15 +17,7 @@ var ( ) func TestSign(t *testing.T) { - coreDoc := testingutils.GenerateCoreDocument() - coreDoc.SigningRoot = key1Pub - idConfig := config.IdentityConfig{ - ID: id1, - PublicKey: key1Pub, - PrivateKey: key1, - } - - sig := Sign(&idConfig, coreDoc.SigningRoot) + sig := Sign(id1, key1, key1Pub, key1Pub) assert.NotNil(t, sig) assert.Equal(t, sig.PublicKey, []byte(key1Pub)) assert.Equal(t, sig.EntityId, id1) @@ -41,23 +27,11 @@ func TestSign(t *testing.T) { assert.NotNil(t, sig.Timestamp, "must be non nil") } -func TestValidateSignature_invalid_key(t *testing.T) { - sig := &coredocumentpb.Signature{EntityId: utils.RandomSlice(identity.CentIDLength)} - srv := &testingcommons.MockIDService{} - centId, _ := identity.ToCentID(sig.EntityId) - srv.On("LookupIdentityForID", centId).Return(nil, fmt.Errorf("failed GetIdentity")).Once() - identity.IDService = srv - err := ValidateSignature(sig, key1Pub) - srv.AssertExpectations(t) - assert.NotNil(t, err, "must be not nil") - assert.Contains(t, err.Error(), "failed GetIdentity") -} - func TestValidateSignature_invalid_sig(t *testing.T) { pubKey := key1Pub message := key1Pub signature := utils.RandomSlice(32) - err := verifySignature(pubKey, message, signature) + err := VerifySignature(pubKey, message, signature) assert.NotNil(t, err, "must be not nil") assert.Contains(t, err.Error(), "invalid signature") } @@ -65,27 +39,6 @@ func TestValidateSignature_invalid_sig(t *testing.T) { func TestValidateSignature_success(t *testing.T) { pubKey := key1Pub message := key1Pub - err := verifySignature(pubKey, message, signature) + err := VerifySignature(pubKey, message, signature) assert.Nil(t, err, "must be nil") } - -func TestValidateCentrifugeId(t *testing.T) { - - randomBytes := utils.RandomSlice(identity.CentIDLength) - - centrifugeId, err := identity.ToCentID(randomBytes) - - assert.Nil(t, err, "centrifugeId not initialized correctly ") - - sig := &coredocumentpb.Signature{EntityId: randomBytes} - - err = ValidateCentrifugeID(sig, centrifugeId) - assert.Nil(t, err, "Validate centrifuge id didn't work correctly") - - randomBytes = utils.RandomSlice(identity.CentIDLength) - centrifugeId, err = identity.ToCentID(randomBytes) - - err = ValidateCentrifugeID(sig, centrifugeId) - assert.NotNil(t, err, "Validate centrifuge id didn't work correctly") - -} diff --git a/storage/bootstrapper.go b/storage/bootstrapper.go index 36d91ad2c..601282035 100644 --- a/storage/bootstrapper.go +++ b/storage/bootstrapper.go @@ -1,26 +1,44 @@ package storage import ( - "errors" - "fmt" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/errors" +) + +const ( + // BootstrappedLevelDB is a key mapped to levelDB at boot + BootstrappedLevelDB string = "BootstrappedLevelDB" + // BootstrappedConfigLevelDB is a key mapped to levelDB for configs at boot + BootstrappedConfigLevelDB string = "BootstrappedConfigLevelDB" ) -type Bootstrapper struct { +// Config holds configuration data for storage package +type Config interface { + GetStoragePath() string + GetConfigStoragePath() string + SetDefault(key string, value interface{}) } +// Bootstrapper implements bootstrapper.Bootstrapper. +type Bootstrapper struct{} + +// Bootstrap initialises the levelDB. func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { if _, ok := context[bootstrap.BootstrappedConfig]; !ok { return errors.New("config not initialised") } + cfg := context[bootstrap.BootstrappedConfig].(Config) - levelDB, err := NewLevelDBStorage(config.Config.GetStoragePath()) + configLevelDB, err := NewLevelDBStorage(cfg.GetConfigStoragePath()) if err != nil { - return fmt.Errorf("failed to init level db: %v", err) + return errors.New("failed to init config level db: %v", err) } + context[BootstrappedConfigLevelDB] = configLevelDB - context[bootstrap.BootstrappedLevelDb] = levelDB + levelDB, err := NewLevelDBStorage(cfg.GetStoragePath()) + if err != nil { + return errors.New("failed to init level db: %v", err) + } + context[BootstrappedLevelDB] = levelDB return nil } diff --git a/storage/leveldb.go b/storage/leveldb.go index 0c6143ad4..05b100406 100644 --- a/storage/leveldb.go +++ b/storage/leveldb.go @@ -1,60 +1,17 @@ package storage import ( - "fmt" - "sync" - logging "github.com/ipfs/go-log" "github.com/syndtr/goleveldb/leveldb" ) var log = logging.Logger("storage") -// levelDBInstance is levelDB instance -var levelDBInstance *leveldb.DB - -// lock to guard the levelDB instance -var lock sync.Mutex - -// GetStorage is a singleton implementation returning the default database as configured +// NewLevelDBStorage is a singleton implementation returning the default database as configured. func NewLevelDBStorage(path string) (*leveldb.DB, error) { - if levelDBInstance != nil { - return nil, fmt.Errorf("db already open") - } - - lock.Lock() - defer lock.Unlock() i, err := leveldb.OpenFile(path, nil) if err != nil { - log.Fatal(err) - } - - levelDBInstance = i - return levelDBInstance, nil -} - -// GetLevelDBStorage returns levelDB instance if initialised -// panics if not initialised -func GetLevelDBStorage() *leveldb.DB { - if levelDBInstance == nil { - log.Fatalf("LevelDB not initialised") + return nil, err } - - return levelDBInstance -} - -// CloseLevelDBStorage closes any open instance of levelDB -func CloseLevelDBStorage() { - if levelDBInstance == nil { - return - } - - lock.Lock() - defer lock.Unlock() - err := levelDBInstance.Close() - if err != nil { - log.Infof("failed to close the level DB: %v", err) - } - - levelDBInstance = nil + return i, nil } diff --git a/storage/leveldb_test.go b/storage/leveldb_test.go index 01e218d89..66831495e 100644 --- a/storage/leveldb_test.go +++ b/storage/leveldb_test.go @@ -9,23 +9,8 @@ import ( ) func TestNewLevelDBStorage(t *testing.T) { - path := getRandomTestStoragePath() + path := GetRandomTestStoragePath() db, err := NewLevelDBStorage(path) assert.Nil(t, err) assert.NotNil(t, db) - assert.NotNil(t, levelDBInstance) - assert.Equal(t, db, levelDBInstance) - - gdb := GetLevelDBStorage() - assert.NotNil(t, gdb) - assert.Equal(t, db, gdb) - - // fail - db, err = NewLevelDBStorage(path) - assert.Error(t, err) - assert.Contains(t, err.Error(), "db already open") - assert.Nil(t, db) - - CloseLevelDBStorage() - assert.Nil(t, levelDBInstance) } diff --git a/storage/test_bootstrapper.go b/storage/test_bootstrapper.go index 22ad678dd..575f61fa7 100644 --- a/storage/test_bootstrapper.go +++ b/storage/test_bootstrapper.go @@ -3,35 +3,49 @@ package storage import ( - "fmt" - "github.com/centrifuge/go-centrifuge/bootstrap" - "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/utils" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/syndtr/goleveldb/leveldb" ) -const testStoragePath = "/tmp/centrifuge_data.leveldb_TESTING" +var db *leveldb.DB +var configdb *leveldb.DB + +func (*Bootstrapper) TestBootstrap(context map[string]interface{}) (err error) { + cfg := context[bootstrap.BootstrappedConfig].(Config) -func (*Bootstrapper) TestBootstrap(context map[string]interface{}) error { - rs := getRandomTestStoragePath() - config.Config.V.SetDefault("storage.Path", rs) - log.Info("Set storage.Path to:", config.Config.GetStoragePath()) - levelDB, err := NewLevelDBStorage(config.Config.GetStoragePath()) + crs := GetRandomTestStoragePath() + cfg.SetDefault("configStorage.path", crs) + log.Info("Set configStorage.path to:", cfg.GetConfigStoragePath()) + configdb, err = NewLevelDBStorage(cfg.GetConfigStoragePath()) if err != nil { - return fmt.Errorf("failed to init level db: %v", err) + return errors.New("failed to init config level db: %v", err) } + context[BootstrappedConfigLevelDB] = configdb - log.Infof("Setting levelDb at: %s", config.Config.GetStoragePath()) - context[bootstrap.BootstrappedLevelDb] = levelDB - return nil -} - -func (*Bootstrapper) TestTearDown() error { - CloseLevelDBStorage() - // os.RemoveAll(config.Config.GetStoragePath()) + rs := GetRandomTestStoragePath() + cfg.SetDefault("storage.Path", rs) + log.Info("Set storage.Path to:", cfg.GetStoragePath()) + db, err = NewLevelDBStorage(cfg.GetStoragePath()) + if err != nil { + return errors.New("failed to init level db: %v", err) + } + log.Infof("Setting levelDb at: %s", cfg.GetStoragePath()) + context[BootstrappedLevelDB] = db return nil } -func getRandomTestStoragePath() string { - return fmt.Sprintf("%s_%x", testStoragePath, utils.RandomByte32()) +func (b *Bootstrapper) TestTearDown() error { + var err error + dbs := []*leveldb.DB{db, configdb} + for _, idb := range dbs { + if ierr := idb.Close(); ierr != nil { + if err == nil { + err = errors.New("%s", ierr) + } else { + err = errors.AppendError(err, ierr) + } + } + } + return err } diff --git a/storage/util.go b/storage/util.go new file mode 100644 index 000000000..369cbc350 --- /dev/null +++ b/storage/util.go @@ -0,0 +1,14 @@ +package storage + +import ( + "fmt" + + "github.com/centrifuge/go-centrifuge/utils" +) + +const testStoragePath = "/tmp/centrifuge_data.leveldb_TESTING" + +// GetRandomTestStoragePath generates a random path for DB storage +func GetRandomTestStoragePath() string { + return fmt.Sprintf("%s_%x", testStoragePath, utils.RandomByte32()) +} diff --git a/testingutils/commons/mock_ethclient.go b/testingutils/commons/mock_ethclient.go index 6139c1cd0..7e3b1fdee 100644 --- a/testingutils/commons/mock_ethclient.go +++ b/testingutils/commons/mock_ethclient.go @@ -5,10 +5,11 @@ package testingcommons import ( "net/url" + "context" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/mock" ) @@ -16,18 +17,19 @@ type MockEthClient struct { mock.Mock } -func (m *MockEthClient) GetClient() *ethclient.Client { +func (m *MockEthClient) GetGethCallOpts() (*bind.CallOpts, context.CancelFunc) { args := m.Called() - c, _ := args.Get(0).(*ethclient.Client) - return c + c, _ := args.Get(0).(*bind.CallOpts) + return c, func() {} } -func (m *MockEthClient) GetRpcClient() *rpc.Client { +func (m *MockEthClient) GetEthClient() *ethclient.Client { args := m.Called() - return args.Get(0).(*rpc.Client) + c, _ := args.Get(0).(*ethclient.Client) + return c } -func (m *MockEthClient) GetHost() *url.URL { +func (m *MockEthClient) GetNodeURL() *url.URL { args := m.Called() return args.Get(0).(*url.URL) } diff --git a/testingutils/commons/mock_identity.go b/testingutils/commons/mock_identity.go index 0cbb325cb..bcaf2d436 100644 --- a/testingutils/commons/mock_identity.go +++ b/testingutils/commons/mock_identity.go @@ -8,6 +8,7 @@ package testingcommons import ( "context" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/identity" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/mock" @@ -18,6 +19,39 @@ type MockIDService struct { mock.Mock } +func (srv *MockIDService) ValidateSignature(signature *coredocumentpb.Signature, message []byte) error { + args := srv.Called(signature, message) + return args.Error(0) +} + +func (srv *MockIDService) GetClientP2PURL(centID identity.CentID) (url string, err error) { + args := srv.Called(centID) + addr := args.Get(0).(string) + return addr, args.Error(1) +} + +func (srv *MockIDService) GetClientsP2PURLs(centIDs []identity.CentID) ([]string, error) { + args := srv.Called(centIDs) + addr := args.Get(0).([]string) + return addr, args.Error(1) +} + +func (srv *MockIDService) GetIdentityKey(id identity.CentID, pubKey []byte) (keyInfo identity.Key, err error) { + args := srv.Called(id, pubKey) + addr := args.Get(0).(identity.Key) + return addr, args.Error(1) +} + +func (srv *MockIDService) ValidateKey(centrifugeId identity.CentID, key []byte, purpose int) error { + args := srv.Called(centrifugeId, key, purpose) + return args.Error(0) +} + +func (srv *MockIDService) AddKeyFromConfig(purpose int) error { + args := srv.Called(purpose) + return args.Error(0) +} + func (srv *MockIDService) GetIdentityAddress(centID identity.CentID) (common.Address, error) { args := srv.Called(centID) addr := args.Get(0).(common.Address) @@ -54,21 +88,21 @@ func (i *MockID) String() string { return args.String(0) } -func (i *MockID) GetCentrifugeID() identity.CentID { +func (i *MockID) CentID() identity.CentID { args := i.Called() return args.Get(0).(identity.CentID) } -func (i *MockID) CentrifugeID(centId identity.CentID) { +func (i *MockID) SetCentrifugeID(centId identity.CentID) { i.Called(centId) } -func (i *MockID) GetCurrentP2PKey() (ret string, err error) { +func (i *MockID) CurrentP2PKey() (ret string, err error) { args := i.Called() return args.String(0), args.Error(1) } -func (i *MockID) GetLastKeyForPurpose(keyPurpose int) (key []byte, err error) { +func (i *MockID) LastKeyForPurpose(keyPurpose int) (key []byte, err error) { args := i.Called(keyPurpose) return args.Get(0).([]byte), args.Error(1) } @@ -78,11 +112,6 @@ func (i *MockID) AddKeyToIdentity(ctx context.Context, keyPurpose int, key []byt return args.Get(0).(chan *identity.WatchIdentity), args.Error(1) } -func (i *MockID) CheckIdentityExists() (exists bool, err error) { - args := i.Called() - return args.Bool(0), args.Error(1) -} - func (i *MockID) FetchKey(key []byte) (identity.Key, error) { args := i.Called(key) idKey, _ := args.Get(0).(identity.Key) diff --git a/testingutils/config/config.go b/testingutils/config/config.go new file mode 100644 index 000000000..c6f69f1bf --- /dev/null +++ b/testingutils/config/config.go @@ -0,0 +1,155 @@ +package testingconfig + +import ( + "math/big" + "time" + + "github.com/centrifuge/go-centrifuge/config" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +type MockConfig struct { + config.Configuration + mock.Mock +} + +func (m *MockConfig) GetStoragePath() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetP2PPort() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *MockConfig) GetP2PExternalIP() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetP2PConnectionTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *MockConfig) GetReceiveEventNotificationEndpoint() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetServerPort() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *MockConfig) GetServerAddress() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetNumWorkers() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *MockConfig) GetWorkerWaitTimeMS() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *MockConfig) GetEthereumNodeURL() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetEthereumContextReadWaitTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *MockConfig) GetEthereumContextWaitTimeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *MockConfig) GetEthereumIntervalRetry() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + +func (m *MockConfig) GetEthereumMaxRetries() int { + args := m.Called() + return args.Get(0).(int) +} + +func (m *MockConfig) GetEthereumGasPrice() *big.Int { + args := m.Called() + return args.Get(0).(*big.Int) +} + +func (m *MockConfig) GetEthereumGasLimit() uint64 { + args := m.Called() + return args.Get(0).(uint64) +} + +func (m *MockConfig) GetEthereumDefaultAccountName() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetEthereumAccount(accountName string) (account *config.AccountConfig, err error) { + args := m.Called() + return args.Get(0).(*config.AccountConfig), args.Error(1) +} + +func (m *MockConfig) GetTxPoolAccessEnabled() bool { + args := m.Called() + return args.Get(0).(bool) +} + +func (m *MockConfig) GetNetworkString() string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetNetworkKey(k string) string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetContractAddressString(address string) string { + args := m.Called() + return args.Get(0).(string) +} + +func (m *MockConfig) GetContractAddress(address string) common.Address { + args := m.Called() + return args.Get(0).(common.Address) +} + +func (m *MockConfig) GetBootstrapPeers() []string { + args := m.Called() + return args.Get(0).([]string) +} + +func (m *MockConfig) GetNetworkID() uint32 { + args := m.Called() + return args.Get(0).(uint32) +} + +func (m *MockConfig) GetIdentityID() ([]byte, error) { + args := m.Called() + return args.Get(0).([]byte), args.Error(1) +} + +func (m *MockConfig) GetSigningKeyPair() (pub, priv string) { + args := m.Called() + return args.Get(0).(string), args.Get(1).(string) +} + +func (m *MockConfig) GetEthAuthKeyPair() (pub, priv string) { + args := m.Called() + return args.Get(0).(string), args.Get(1).(string) +} diff --git a/testingutils/coredocument/coredocument.go b/testingutils/coredocument/coredocument.go new file mode 100644 index 000000000..0fbb25007 --- /dev/null +++ b/testingutils/coredocument/coredocument.go @@ -0,0 +1,88 @@ +// +build integration unit + +package testingcoredocument + +import ( + "github.com/centrifuge/centrifuge-protobufs/documenttypes" + "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" + "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/header" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/utils" + "github.com/centrifuge/precise-proofs/proofs" + "github.com/golang/protobuf/ptypes/any" + "github.com/stretchr/testify/mock" +) + +func GenerateCoreDocument() *coredocumentpb.CoreDocument { + identifier := utils.RandomSlice(32) + salts := &coredocumentpb.CoreDocumentSalts{} + doc := &coredocumentpb.CoreDocument{ + DataRoot: utils.RandomSlice(32), + DocumentIdentifier: identifier, + CurrentVersion: identifier, + NextVersion: utils.RandomSlice(32), + CoredocumentSalts: salts, + EmbeddedData: &any.Any{ + TypeUrl: documenttypes.InvoiceDataTypeUrl, + }, + EmbeddedDataSalts: &any.Any{ + TypeUrl: documenttypes.InvoiceSaltsTypeUrl, + }, + } + proofs.FillSalts(doc, salts) + return doc +} + +type MockCoreDocumentProcessor struct { + mock.Mock +} + +func (m *MockCoreDocumentProcessor) Send(ctx *header.ContextHeader, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) { + args := m.Called(coreDocument, ctx, recipient) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) Anchor( + ctx *header.ContextHeader, + coreDocument *coredocumentpb.CoreDocument, + saveState func(*coredocumentpb.CoreDocument) error) (err error) { + args := m.Called(ctx, coreDocument, saveState) + if saveState != nil { + err := saveState(coreDocument) + if err != nil { + return err + } + } + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) PrepareForSignatureRequests(ctx *header.ContextHeader, model documents.Model) error { + args := m.Called(model) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) RequestSignatures(ctx *header.ContextHeader, model documents.Model) error { + args := m.Called(ctx, model) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) PrepareForAnchoring(model documents.Model) error { + args := m.Called(model) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) AnchorDocument(ctx *header.ContextHeader, model documents.Model) error { + args := m.Called(model) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) SendDocument(ctx *header.ContextHeader, model documents.Model) error { + args := m.Called(ctx, model) + return args.Error(0) +} + +func (m *MockCoreDocumentProcessor) GetDataProofHashes(coreDocument *coredocumentpb.CoreDocument) (hashes [][]byte, err error) { + args := m.Called(coreDocument) + return args.Get(0).([][]byte), args.Error(1) +} diff --git a/testingutils/documents/documents.go b/testingutils/documents/documents.go index 02638ed38..6e31a3c9d 100644 --- a/testingutils/documents/documents.go +++ b/testingutils/documents/documents.go @@ -6,6 +6,7 @@ import ( "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/centrifuge-protobufs/gen/go/p2p" "github.com/centrifuge/go-centrifuge/documents" + "github.com/centrifuge/go-centrifuge/header" "github.com/stretchr/testify/mock" ) @@ -38,7 +39,7 @@ func (m *MockService) DeriveFromCoreDocument(cd *coredocumentpb.CoreDocument) (d return args.Get(0).(documents.Model), args.Error(1) } -func (m *MockService) RequestDocumentSignature(model documents.Model) (*coredocumentpb.Signature, error) { +func (m *MockService) RequestDocumentSignature(ctx *header.ContextHeader, model documents.Model) (*coredocumentpb.Signature, error) { args := m.Called() return args.Get(0).(*coredocumentpb.Signature), args.Error(1) } @@ -48,6 +49,11 @@ func (m *MockService) ReceiveAnchoredDocument(model documents.Model, headers *p2 return args.Error(0) } +func (m *MockService) Exists(documentID []byte) bool { + args := m.Called() + return args.Get(0).(bool) +} + type MockModel struct { documents.Model mock.Mock diff --git a/testingutils/identity/identity.go b/testingutils/identity/identity.go new file mode 100644 index 000000000..a33d29c38 --- /dev/null +++ b/testingutils/identity/identity.go @@ -0,0 +1,39 @@ +// +build integration unit + +package testingidentity + +import ( + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/ethereum" + "github.com/centrifuge/go-centrifuge/identity" +) + +func CreateIdentityWithKeys(cfg config.Configuration, idService identity.Service) identity.CentID { + idConfig, _ := identity.GetIdentityConfig(cfg) + // only create identity if it doesn't exist + id, err := idService.LookupIdentityForID(idConfig.ID) + if err != nil { + _, confirmations, _ := idService.CreateIdentity(idConfig.ID) + <-confirmations + // LookupIdentityForId + id, _ = idService.LookupIdentityForID(idConfig.ID) + } + + // only add key if it doesn't exist + _, err = id.LastKeyForPurpose(identity.KeyPurposeEthMsgAuth) + ctx, cancel := ethereum.DefaultWaitForTransactionMiningContext(cfg.GetEthereumContextWaitTimeout()) + defer cancel() + if err != nil { + confirmations, _ := id.AddKeyToIdentity(ctx, identity.KeyPurposeEthMsgAuth, idConfig.Keys[identity.KeyPurposeEthMsgAuth].PublicKey) + <-confirmations + } + _, err = id.LastKeyForPurpose(identity.KeyPurposeSigning) + ctx, cancel = ethereum.DefaultWaitForTransactionMiningContext(cfg.GetEthereumContextWaitTimeout()) + defer cancel() + if err != nil { + confirmations, _ := id.AddKeyToIdentity(ctx, identity.KeyPurposeSigning, idConfig.Keys[identity.KeyPurposeSigning].PublicKey) + <-confirmations + } + + return idConfig.ID +} diff --git a/testingutils/utils.go b/testingutils/utils.go index 2e902fb58..3a3e291a5 100644 --- a/testingutils/utils.go +++ b/testingutils/utils.go @@ -3,108 +3,15 @@ package testingutils import ( - "context" - "crypto/rand" - - "github.com/centrifuge/centrifuge-protobufs/documenttypes" - "github.com/centrifuge/centrifuge-protobufs/gen/go/coredocument" "github.com/centrifuge/go-centrifuge/config" - "github.com/centrifuge/go-centrifuge/documents" - "github.com/centrifuge/go-centrifuge/ethereum" - "github.com/centrifuge/go-centrifuge/identity" - "github.com/centrifuge/go-centrifuge/keytools/ed25519" - "github.com/centrifuge/go-centrifuge/keytools/secp256k1" - "github.com/centrifuge/go-centrifuge/utils" - "github.com/centrifuge/precise-proofs/proofs" - "github.com/golang/protobuf/ptypes/any" - "github.com/stretchr/testify/mock" ) -func MockConfigOption(key string, value interface{}) func() { - mockedValue := config.Config.V.Get(key) - config.Config.V.Set(key, value) +func MockConfigOption(cfg config.Configuration, key string, value interface{}) func() { + mockedValue := cfg.Get(key) + cfg.Set(key, value) return func() { - config.Config.V.Set(key, mockedValue) - } -} - -func Rand32Bytes() []byte { - bytes := make([]byte, 32) - rand.Read(bytes) - return bytes -} - -func GenerateCoreDocument() *coredocumentpb.CoreDocument { - identifier := Rand32Bytes() - salts := &coredocumentpb.CoreDocumentSalts{} - doc := &coredocumentpb.CoreDocument{ - DataRoot: utils.RandomSlice(32), - DocumentIdentifier: identifier, - CurrentVersion: identifier, - NextVersion: Rand32Bytes(), - CoredocumentSalts: salts, - EmbeddedData: &any.Any{ - TypeUrl: documenttypes.InvoiceDataTypeUrl, - }, - EmbeddedDataSalts: &any.Any{ - TypeUrl: documenttypes.InvoiceSaltsTypeUrl, - }, - } - proofs.FillSalts(doc, salts) - return doc -} - -type MockCoreDocumentProcessor struct { - mock.Mock -} - -func (m *MockCoreDocumentProcessor) Send(ctx context.Context, coreDocument *coredocumentpb.CoreDocument, recipient identity.CentID) (err error) { - args := m.Called(coreDocument, ctx, recipient) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) Anchor( - ctx context.Context, - coreDocument *coredocumentpb.CoreDocument, - saveState func(*coredocumentpb.CoreDocument) error) (err error) { - args := m.Called(ctx, coreDocument, saveState) - if saveState != nil { - err := saveState(coreDocument) - if err != nil { - return err - } + cfg.Set(key, mockedValue) } - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) PrepareForSignatureRequests(model documents.Model) error { - args := m.Called(model) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) RequestSignatures(ctx context.Context, model documents.Model) error { - args := m.Called(ctx, model) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) PrepareForAnchoring(model documents.Model) error { - args := m.Called(model) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) AnchorDocument(model documents.Model) error { - args := m.Called(model) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) SendDocument(ctx context.Context, model documents.Model) error { - args := m.Called(ctx, model) - return args.Error(0) -} - -func (m *MockCoreDocumentProcessor) GetDataProofHashes(coreDocument *coredocumentpb.CoreDocument) (hashes [][]byte, err error) { - args := m.Called(coreDocument) - return args.Get(0).([][]byte), args.Error(1) } type MockSubscription struct { @@ -116,35 +23,3 @@ func (m *MockSubscription) Err() <-chan error { } func (*MockSubscription) Unsubscribe() {} - -func CreateIdentityWithKeys() identity.CentID { - idConfigEth, _ := secp256k1.GetIDConfig() - idConfig, _ := ed25519.GetIDConfig() - centIdTyped, _ := identity.ToCentID(idConfigEth.ID) - // only create identity if it doesn't exist - id, err := identity.IDService.LookupIdentityForID(centIdTyped) - if err != nil { - _, confirmations, _ := identity.IDService.CreateIdentity(centIdTyped) - <-confirmations - // LookupIdentityForId - id, _ = identity.IDService.LookupIdentityForID(centIdTyped) - } - - // only add key if it doesn't exist - _, err = id.GetLastKeyForPurpose(identity.KeyPurposeEthMsgAuth) - ctx, cancel := ethereum.DefaultWaitForTransactionMiningContext() - defer cancel() - if err != nil { - confirmations, _ := id.AddKeyToIdentity(ctx, identity.KeyPurposeEthMsgAuth, idConfigEth.PublicKey) - <-confirmations - } - _, err = id.GetLastKeyForPurpose(identity.KeyPurposeSigning) - ctx, cancel = ethereum.DefaultWaitForTransactionMiningContext() - defer cancel() - if err != nil { - confirmations, _ := id.AddKeyToIdentity(ctx, identity.KeyPurposeSigning, idConfig.PublicKey) - <-confirmations - } - - return centIdTyped -} diff --git a/testworld/README.md b/testworld/README.md new file mode 100644 index 000000000..ed3c1e17a --- /dev/null +++ b/testworld/README.md @@ -0,0 +1,23 @@ +## Welcome to Testworld + +`"CHAOS TAKES CONTROL. WELCOME TO THE NEW WORLD. WELCOME TO TESTWORLD"` + +Testworld (loosely analogous to [Westworld](https://medium.com/@naveen101/westworld-an-introduction-cc7d29bfbe84) ;) is a simulation and test environment for centrifuge p2p network. +Here you can create, run and test nodes with various behaviours to observe how they would behave and debug any problems encountered. + +### Tutorial + +- All hosts (p2p nodes) in the Testworld are created and maintained during simulation by drFord(`park.go#hostManager`). He also ensures that the hosts and tests are properly cleaned up after each simulation run. +- Bernard (`hostManager.bernard`) is a special host that serves as the libp2p bootnode for the test network. +- `hostConfig` serves as the starting point for you to define new hosts. Please check whether an existing host can be reused for your scenario before adding new ones. +- At the start of the each test run a test config is loaded for the required Ethereum network(eg: Rinkeby or local). The host configs are defined based on this. +- The test initialisation also ensures that geth is running in the background and required smart contracts are migrated for the network. +- Refer `park_test.go` for a simple starting point to define your own simulations/tests. +- Each test scenario must be defined in a `testworld/_test.go` file and the build tag `// +build testworld` must be included at the top. +- Plus points if you write a test with a scenario that matches a scene in Westworld with node names matching the characters ;) + +### Dev +#### Speed improvements for local testing +- On `start_test.go` set `runMigrations` to `false` after the contracts are deployed once at the local geth node. +- On `start_test.go` set `createHostConfigs` to `false` after configs have been generated in `peerconfigs` dir, note that if you add new hosts using `hostConfig` you would need this to be set to `true` again to generate the config for the new host. + diff --git a/testworld/config.go b/testworld/config.go new file mode 100644 index 000000000..40d5d50d1 --- /dev/null +++ b/testworld/config.go @@ -0,0 +1,27 @@ +package testworld + +import ( + "encoding/json" + "fmt" + "os" +) + +type testConfig struct { + EthNodeURL string `json:"ethNodeURL"` + AccountKeyPath string `json:"accountKeyPath"` + AccountPassword string `json:"accountPassword"` + Network string `json:"network"` + TxPoolAccess bool `json:"txPoolAccess"` +} + +func loadConfig(file string) (testConfig, error) { + var config testConfig + configFile, err := os.Open(file) + if err != nil { + fmt.Println(err.Error()) + } + defer configFile.Close() + jsonParser := json.NewDecoder(configFile) + jsonParser.Decode(&config) + return config, nil +} diff --git a/testworld/configs/local.json b/testworld/configs/local.json new file mode 100644 index 000000000..35f0215e9 --- /dev/null +++ b/testworld/configs/local.json @@ -0,0 +1,7 @@ +{ + "ethNodeURL": "ws://127.0.0.1:9546", + "accountKeyPath": "../build/scripts/test-dependencies/test-ethereum/migrateAccount.json", + "accountPassword": "", + "network" : "testing", + "txPoolAccess": true +} \ No newline at end of file diff --git a/testworld/document_consensus_test.go b/testworld/document_consensus_test.go new file mode 100644 index 000000000..dc3175bce --- /dev/null +++ b/testworld/document_consensus_test.go @@ -0,0 +1,115 @@ +// +build testworld + +package testworld + +import ( + "net/http" + "testing" +) + +func TestHost_AddExternalCollaborator(t *testing.T) { + t.Parallel() + tests := []struct { + name string + docType string + }{ + { + "Invoice_AddExternalCollaborator", + typeInvoice, + }, + { + "PO_AddExternalCollaborator", + typePO, + }, + } + for _, test := range tests { + t.Run(test.docType, func(t *testing.T) { + t.Parallel() + addExternalCollaborator(t, test.docType) + }) + } +} + +func addExternalCollaborator(t *testing.T, documentType string) { + alice := doctorFord.getHostTestSuite(t, "Alice") + bob := doctorFord.getHostTestSuite(t, "Bob") + charlie := doctorFord.getHostTestSuite(t, "Charlie") + + // Alice shares document with Bob first + res := createDocument(alice.httpExpect, documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{bob.id.String()})) + + docIdentifier := getDocumentIdentifier(t, res) + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + + params := map[string]interface{}{ + "document_id": docIdentifier, + "currency": "USD", + } + getDocumentAndCheck(alice.httpExpect, documentType, params) + getDocumentAndCheck(bob.httpExpect, documentType, params) + + // Bob updates invoice and shares with Charlie as well + res = updateDocument(bob.httpExpect, documentType, http.StatusOK, docIdentifier, updatedDocumentPayload(documentType, []string{alice.id.String(), charlie.id.String()})) + + docIdentifier = getDocumentIdentifier(t, res) + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + params["currency"] = "EUR" + getDocumentAndCheck(alice.httpExpect, documentType, params) + getDocumentAndCheck(bob.httpExpect, documentType, params) + getDocumentAndCheck(charlie.httpExpect, documentType, params) +} + +func TestHost_CollaboratorTimeOut(t *testing.T) { + // Run only locally since this creates resource issues for the entire test suite + t.SkipNow() + t.Parallel() + + //currently can't be run in parallel (because of node kill) + collaboratorTimeOut(t, typeInvoice) + collaboratorTimeOut(t, typePO) +} + +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, documentType, http.StatusOK, defaultInvoicePayload([]string{bob.id.String()})) + + // check if Bob and Kenny received the document + docIdentifier := getDocumentIdentifier(t, response) + paramsV1 := map[string]interface{}{ + "document_id": docIdentifier, + "currency": "USD", + } + getDocumentAndCheck(kenny.httpExpect, documentType, paramsV1) + getDocumentAndCheck(bob.httpExpect, documentType, paramsV1) + + // Kenny gets killed + kenny.host.kill() + + // Bob updates and sends to Alice + updatedPayload := updatedDocumentPayload(documentType, []string{kenny.id.String()}) + + // Bob will anchor the document without Alice signature but will receive an error because kenny is dead + response = updateDocument(bob.httpExpect, documentType, http.StatusInternalServerError, docIdentifier, updatedPayload) + + // check if bob saved the updated document + paramsV2 := map[string]interface{}{ + "document_id": docIdentifier, + "currency": "EUR", + } + getDocumentAndCheck(bob.httpExpect, documentType, paramsV2) + + // bring Kenny back to life + doctorFord.reLive(t, kenny.name) + + // Kenny should NOT have latest version + getDocumentAndCheck(kenny.httpExpect, documentType, paramsV1) + +} diff --git a/testworld/httputils.go b/testworld/httputils.go new file mode 100644 index 000000000..d6a17acdf --- /dev/null +++ b/testworld/httputils.go @@ -0,0 +1,91 @@ +package testworld + +import ( + "crypto/tls" + "net/http" + "testing" + + "github.com/gavv/httpexpect" +) + +const typeInvoice string = "invoice" +const typePO string = "purchaseorder" +const poPrefix string = "po" + +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), + }, + } + return httpexpect.WithConfig(config) +} + +func getDocumentAndCheck(e *httpexpect.Expect, documentType string, params map[string]interface{}) *httpexpect.Value { + docIdentifier := params["document_id"].(string) + + objGet := e.GET("/"+documentType+"/"+docIdentifier). + WithHeader("accept", "application/json"). + WithHeader("Content-Type", "application/json"). + Expect().Status(http.StatusOK).JSON().NotNull() + objGet.Path("$.header.document_id").String().Equal(docIdentifier) + objGet.Path("$.data.currency").String().Equal(params["currency"].(string)) + + return objGet +} + +func createDocument(e *httpexpect.Expect, documentType string, status int, payload map[string]interface{}) *httpexpect.Object { + obj := e.POST("/"+documentType). + WithHeader("accept", "application/json"). + WithHeader("Content-Type", "application/json"). + WithJSON(payload). + Expect().Status(status).JSON().Object() + return obj +} + +func updateDocument(e *httpexpect.Expect, documentType string, status int, docIdentifier string, payload map[string]interface{}) *httpexpect.Object { + obj := e.PUT("/"+documentType+"/"+docIdentifier). + WithHeader("accept", "application/json"). + WithHeader("Content-Type", "application/json"). + WithJSON(payload). + Expect().Status(status).JSON().Object() + return obj +} + +func getDocumentIdentifier(t *testing.T, response *httpexpect.Object) string { + docIdentifier := response.Value("header").Path("$.document_id").String().NotEmpty().Raw() + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + return docIdentifier +} + +func mintNFT(e *httpexpect.Expect, httpStatus int, payload map[string]interface{}) *httpexpect.Object { + resp := e.POST("/token/mint"). + WithHeader("accept", "application/json"). + WithHeader("Content-Type", "application/json"). + WithJSON(payload). + Expect().Status(httpStatus) + + httpObj := resp.JSON().Object() + return httpObj +} + +func getProof(e *httpexpect.Expect, httpStatus int, documentID string, payload map[string]interface{}) *httpexpect.Object { + resp := e.POST("/document/"+documentID+"/proof"). + WithHeader("accept", "application/json"). + WithHeader("Content-Type", "application/json"). + WithJSON(payload). + Expect().Status(httpStatus) + return resp.JSON().Object() +} + +func createInsecureClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + return &http.Client{Transport: tr} +} diff --git a/testworld/nft_test.go b/testworld/nft_test.go new file mode 100644 index 000000000..46a80363e --- /dev/null +++ b/testworld/nft_test.go @@ -0,0 +1,119 @@ +// +build testworld + +package testworld + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +const tokenIdLength = 77 + +func TestPaymentObligationMint_invoice_successful(t *testing.T) { + t.Parallel() + paymentObligationMint(t, typeInvoice) + +} + +/* TODO: testcase not stable +func TestPaymentObligationMint_po_successful(t *testing.T) { + t.Parallel() + paymentObligationMint(t, typePO) + +} +*/ + +func paymentObligationMint(t *testing.T, documentType string) { + + alice := doctorFord.getHostTestSuite(t, "Alice") + bob := doctorFord.getHostTestSuite(t, "Bob") + + // Alice shares document with Bob + res := createDocument(alice.httpExpect, documentType, http.StatusOK, defaultNFTPayload(documentType, []string{bob.id.String()})) + + docIdentifier := getDocumentIdentifier(t, res) + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + + params := map[string]interface{}{ + "document_id": docIdentifier, + "currency": "USD", + } + getDocumentAndCheck(alice.httpExpect, documentType, params) + getDocumentAndCheck(bob.httpExpect, documentType, params) + + proofPrefix := documentType + if proofPrefix == typePO { + proofPrefix = poPrefix + } + + // mint an NFT + test := struct { + httpStatus int + payload map[string]interface{} + }{ + http.StatusOK, + map[string]interface{}{ + + "identifier": docIdentifier, + "registryAddress": doctorFord.contractAddresses.PaymentObligationAddr, + "depositAddress": "0xf72855759a39fb75fc7341139f5d7a3974d4da08", // dummy address + "proofFields": []string{proofPrefix + ".gross_amount", proofPrefix + ".currency", proofPrefix + ".due_date", "collaborators[0]"}, + }, + } + + response, err := alice.host.mintNFT(alice.httpExpect, test.httpStatus, test.payload) + assert.Nil(t, err, "mintNFT should be successful") + assert.True(t, len(response.Value("token_id").String().Raw()) >= tokenIdLength, "successful tokenId should have length 77") + +} + +func TestPaymentObligationMint_errors(t *testing.T) { + t.Parallel() + alice := doctorFord.getHostTestSuite(t, "Alice") + tests := []struct { + errorMsg string + httpStatus int + payload map[string]interface{} + }{ + { + + "RegistryAddress is not a valid Ethereum address", + http.StatusInternalServerError, + map[string]interface{}{ + + "registryAddress": "0x123", + }, + }, + { + "DepositAddress is not a valid Ethereum address", + http.StatusInternalServerError, + map[string]interface{}{ + + "registryAddress": "0xf72855759a39fb75fc7341139f5d7a3974d4da08", //dummy address + "depositAddress": "abc", + }, + }, + { + "no service exists for provided documentID", + http.StatusInternalServerError, + map[string]interface{}{ + + "identifier": "0x12121212", + "registryAddress": "0xf72855759a39fb75fc7341139f5d7a3974d4da08", //dummy address + "depositAddress": "0xf72855759a39fb75fc7341139f5d7a3974d4da08", //dummy address + }, + }, + } + for _, test := range tests { + t.Run(test.errorMsg, func(t *testing.T) { + t.Parallel() + response, err := alice.host.mintNFT(alice.httpExpect, test.httpStatus, test.payload) + assert.Nil(t, err, "it should be possible to call the API endpoint") + response.Value("message").String().Contains(test.errorMsg) + }) + } +} diff --git a/testworld/park.go b/testworld/park.go new file mode 100644 index 000000000..93e4d2557 --- /dev/null +++ b/testworld/park.go @@ -0,0 +1,349 @@ +// +build testworld + +package testworld + +import ( + "context" + "fmt" + "os" + "os/signal" + + "testing" + + "time" + + "net/http" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/cmd" + "github.com/centrifuge/go-centrifuge/config" + ctx "github.com/centrifuge/go-centrifuge/context" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/go-centrifuge/identity" + "github.com/centrifuge/go-centrifuge/node" + "github.com/gavv/httpexpect" + logging "github.com/ipfs/go-log" +) + +var log = logging.Logger("host") + +var hostConfig = []struct { + name string + apiPort, p2pPort int64 +}{ + {"Alice", 8084, 38204}, + {"Bob", 8085, 38205}, + {"Charlie", 8086, 38206}, + {"Kenny", 8087, 38207}, +} + +const defaultP2PTimeout = "10s" + +// hostTestSuite encapsulates test utilities on top of each host +type hostTestSuite struct { + name string + host *host + id identity.CentID + httpExpect *httpexpect.Expect +} + +// hostManager is the hostManager of the hosts at Testworld (Robert) +type hostManager struct { + + // network settings + ethNodeUrl, accountKeyPath, accountPassword, network string + + txPoolAccess bool + + // contractAddresses are the addresses of centrifuge contracts on Ethereum + contractAddresses *config.SmartContractAddresses + + // bernard is the bootnode for all the hosts + bernard *host + + // niceHosts are the happy and nice hosts at the Testworld such as Teddy + niceHosts map[string]*host + + // TODO create evil hosts such as William (or Eve) + + // canc is the cancel signal for all hosts + canc context.CancelFunc + + // TODO: context should be removed from hostManager + // currently needed to restart a node + // parent context + cancCtx context.Context +} + +func newHostManager( + ethNodeUrl, accountKeyPath, accountPassword, network string, + txPoolAccess bool, + smartContractAddrs *config.SmartContractAddresses) *hostManager { + return &hostManager{ + ethNodeUrl: ethNodeUrl, + accountKeyPath: accountKeyPath, + accountPassword: accountPassword, + network: network, + txPoolAccess: txPoolAccess, + contractAddresses: smartContractAddrs, + niceHosts: make(map[string]*host), + } +} + +func (r *hostManager) reLive(t *testing.T, name string) { + r.startHost(name) + // wait for the host to be live, here its 11 seconds allowed but the host should come alive before that and this will return faster + ok, err := r.getHost(name).isLive(11 * time.Second) + if ok { + return + } else { + t.Error(err) + } +} + +func (r *hostManager) startHost(name string) { + go r.niceHosts[name].live(r.cancCtx) +} + +func (r *hostManager) init(createConfig bool) error { + r.cancCtx, r.canc = context.WithCancel(context.Background()) + r.bernard = r.createHost("Bernard", defaultP2PTimeout, 8081, 38201, createConfig, nil) + err := r.bernard.init() + if err != nil { + return err + } + + // start and wait for Bernard since other hosts depend on him + go r.bernard.live(r.cancCtx) + _, err = r.bernard.isLive(10 * time.Second) + if err != nil { + return errors.New("bernard couldn't be made alive %v", err) + } + + bootnode, err := r.bernard.p2pURL() + if err != nil { + return err + } + + // start hosts + for _, h := range hostConfig { + r.niceHosts[h.name] = r.createHost(h.name, defaultP2PTimeout, h.apiPort, h.p2pPort, createConfig, []string{bootnode}) + + err := r.niceHosts[h.name].init() + if err != nil { + return err + } + r.startHost(h.name) + + } + // make sure hosts are alive and print host centIDs + for name, host := range r.niceHosts { + _, err = host.isLive(10 * time.Second) + if err != nil { + return errors.New("%s couldn't be made alive %v", host.name, err) + } + i, err := host.id() + if err != nil { + return err + } + fmt.Printf("CentID for %s is %s \n", name, i) + } + return nil +} + +func (r *hostManager) getHost(name string) *host { + if h, ok := r.niceHosts[name]; ok { + return h + } + return nil +} + +func (r *hostManager) stop() { + r.canc() +} + +func (r *hostManager) createHost(name, p2pTimeout string, apiPort, p2pPort int64, createConfig bool, bootstraps []string) *host { + return newHost( + name, + r.ethNodeUrl, + r.accountKeyPath, + r.accountPassword, + r.network, + p2pTimeout, + apiPort, p2pPort, bootstraps, + r.txPoolAccess, + createConfig, + r.contractAddresses, + ) +} + +func (r *hostManager) getHostTestSuite(t *testing.T, name string) hostTestSuite { + host := r.getHost(name) + expect := host.createHttpExpectation(t) + id, err := host.id() + if err != nil { + t.Error(err) + } + return hostTestSuite{name: name, host: host, id: id, httpExpect: expect} + +} + +type host struct { + name, dir, ethNodeUrl, accountKeyPath, accountPassword, network, + identityFactoryAddr, identityRegistryAddr, anchorRepositoryAddr, paymentObligationAddr, p2pTimeout string + apiPort, p2pPort int64 + bootstrapNodes []string + bootstrappedCtx map[string]interface{} + txPoolAccess bool + smartContractAddrs *config.SmartContractAddresses + config config.Configuration + identity identity.Identity + node *node.Node + canc context.CancelFunc + createConfig bool +} + +func newHost( + name, ethNodeUrl, accountKeyPath, accountPassword, network, p2pTimeout string, + apiPort, p2pPort int64, + bootstraps []string, + txPoolAccess, createConfig bool, + smartContractAddrs *config.SmartContractAddresses, +) *host { + return &host{ + name: name, + ethNodeUrl: ethNodeUrl, + accountKeyPath: accountKeyPath, + accountPassword: accountPassword, + network: network, + apiPort: apiPort, + p2pPort: p2pPort, + p2pTimeout: p2pTimeout, + bootstrapNodes: bootstraps, + txPoolAccess: txPoolAccess, + smartContractAddrs: smartContractAddrs, + dir: "peerconfigs/" + name, + createConfig: createConfig, + } +} + +func (h *host) init() error { + if h.createConfig { + err := cmd.CreateConfig(h.dir, h.ethNodeUrl, h.accountKeyPath, h.accountPassword, h.network, h.apiPort, h.p2pPort, h.bootstrapNodes, h.txPoolAccess, h.p2pTimeout, h.smartContractAddrs) + if err != nil { + return err + } + } + + m := ctx.MainBootstrapper{} + m.PopulateBaseBootstrappers() + h.bootstrappedCtx = map[string]interface{}{ + config.BootstrappedConfigFile: h.dir + "/config.yaml", + } + err := m.Bootstrap(h.bootstrappedCtx) + if err != nil { + return err + } + h.config = h.bootstrappedCtx[bootstrap.BootstrappedConfig].(config.Configuration) + idService := h.bootstrappedCtx[identity.BootstrappedIDService].(identity.Service) + idBytes, err := h.config.GetIdentityID() + if err != nil { + return err + } + id, err := identity.ToCentID(idBytes) + if err != nil { + return err + } + h.identity, err = idService.LookupIdentityForID(id) + if err != nil { + return err + } + return nil +} + +func (h *host) live(c context.Context) error { + srvs, err := node.GetServers(h.bootstrappedCtx) + if err != nil { + return errors.New("failed to load servers: %v", err) + } + + h.node = node.New(srvs) + feedback := make(chan error) + // may be we can pass a context that exists in c here + cancCtx, canc := context.WithCancel(context.WithValue(c, bootstrap.NodeObjRegistry, h.bootstrappedCtx)) + + // cancel func of individual host + h.canc = canc + + go h.node.Start(cancCtx, feedback) + controlC := make(chan os.Signal, 1) + signal.Notify(controlC, os.Interrupt) + select { + case err := <-feedback: + log.Info(h.name+" encountered error ", err) + return err + case sig := <-controlC: + log.Info(h.name+" shutting down because of ", sig) + canc() + err := <-feedback + return err + } + +} + +func (h *host) kill() { + h.canc() +} + +// isLive waits for host to come alive until the given soft timeout has passed, or the hard timeout of 10s is passed +func (h *host) isLive(softTimeOut time.Duration) (bool, error) { + sig := make(chan error) + c := createInsecureClient() + go func(sig chan<- error) { + var fErr error + // wait upto 10 seconds(hard timeout) for the host to be live + for i := 0; i < 10; i++ { + res, err := c.Get(fmt.Sprintf("https://localhost:%d/ping", h.config.GetServerPort())) + fErr = err + if err != nil { + time.Sleep(time.Second) + continue + } + if res.StatusCode == http.StatusOK { + sig <- nil + return + } + } + sig <- fErr + }(sig) + t := time.After(softTimeOut) + select { + case <-t: + return false, errors.New("host failed to live even after %f seconds", softTimeOut.Seconds()) + case err := <-sig: + if err != nil { + return false, err + } + return true, nil + } +} + +func (h *host) mintNFT(e *httpexpect.Expect, status int, inv map[string]interface{}) (*httpexpect.Object, error) { + return mintNFT(e, status, inv), nil +} + +func (h *host) createHttpExpectation(t *testing.T) *httpexpect.Expect { + return createInsecureClientWithExpect(t, fmt.Sprintf("https://localhost:%d", h.config.GetServerPort())) +} + +func (h *host) id() (identity.CentID, error) { + return h.identity.CentID(), nil +} + +func (h *host) p2pURL() (string, error) { + lastB58Key, err := h.identity.CurrentP2PKey() + if err != nil { + return "", err + } + return fmt.Sprintf("/ip4/127.0.0.1/tcp/%d/ipfs/%s", h.p2pPort, lastB58Key), nil +} diff --git a/testworld/park_test.go b/testworld/park_test.go new file mode 100644 index 000000000..e0bea509d --- /dev/null +++ b/testworld/park_test.go @@ -0,0 +1,33 @@ +// +build testworld + +package testworld + +import ( + "fmt" + "net/http" + "testing" +) + +func TestHost_Happy(t *testing.T) { + t.Parallel() + alice := doctorFord.getHostTestSuite(t, "Alice") + bob := doctorFord.getHostTestSuite(t, "Bob") + charlie := doctorFord.getHostTestSuite(t, "Charlie") + + // alice shares a document with bob and charlie + res := createDocument(alice.httpExpect, typeInvoice, http.StatusOK, defaultInvoicePayload([]string{bob.id.String(), charlie.id.String()})) + + docIdentifier := getDocumentIdentifier(t, res) + + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + params := map[string]interface{}{ + "document_id": docIdentifier, + "currency": "USD", + } + getDocumentAndCheck(alice.httpExpect, typeInvoice, params) + getDocumentAndCheck(bob.httpExpect, typeInvoice, params) + getDocumentAndCheck(charlie.httpExpect, typeInvoice, params) + fmt.Println("Host test success") +} diff --git a/testworld/payloads.go b/testworld/payloads.go new file mode 100644 index 000000000..684d6ae76 --- /dev/null +++ b/testworld/payloads.go @@ -0,0 +1,139 @@ +// +build testworld + +package testworld + +func defaultDocumentPayload(documentType string, collaborators []string) map[string]interface{} { + + switch documentType { + case typeInvoice: + return defaultInvoicePayload(collaborators) + case typePO: + return defaultPOPayload(collaborators) + default: + return defaultInvoicePayload(collaborators) + } + +} + +func defaultPOPayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "po_number": "12324", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "USD", + "net_amount": "40", + }, + "collaborators": collaborators, + } + +} +func defaultInvoicePayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "invoice_number": "12324", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "USD", + "net_amount": "40", + }, + "collaborators": collaborators, + } + +} + +func invoiceNFTPayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "invoice_number": "12324", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "USD", + "net_amount": "40", + "document_type": "invoice", + }, + "collaborators": collaborators, + } + +} + +func pONFTPayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "po_number": "123245", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "USD", + "net_amount": "40", + "document_type": "po", + }, + "collaborators": collaborators, + } + +} + +func defaultNFTPayload(documentType string, collaborators []string) map[string]interface{} { + + switch documentType { + case typeInvoice: + return invoiceNFTPayload(collaborators) + case typePO: + return pONFTPayload(collaborators) + default: + return invoiceNFTPayload(collaborators) + } + +} + +func updatedDocumentPayload(documentType string, collaborators []string) map[string]interface{} { + switch documentType { + case typeInvoice: + return updatedInvoicePayload(collaborators) + case typePO: + return updatedPOPayload(collaborators) + default: + return updatedInvoicePayload(collaborators) + } +} + +func updatedPOPayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "po_number": "12324", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "EUR", + "net_amount": "42", + }, + "collaborators": collaborators, + } + +} + +func updatedInvoicePayload(collaborators []string) map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "invoice_number": "12324", + "due_date": "2018-09-26T23:12:37.902198664Z", + "gross_amount": "40", + "currency": "EUR", + "net_amount": "42", + }, + "collaborators": collaborators, + } + +} + +func defaultProofPayload(documentType string) map[string]interface{} { + if documentType == typeInvoice { + + return map[string]interface{}{ + "type": "http://github.com/centrifuge/centrifuge-protobufs/invoice/#invoice.InvoiceData", + "fields": []string{"invoice.net_amount", "invoice.currency"}, + } + } + return map[string]interface{}{ + "type": "http://github.com/centrifuge/centrifuge-protobufs/purchaseorder/#purchaseorder.PurchaseOrderData", + "fields": []string{"po.net_amount", "po.currency"}, + } +} diff --git a/testworld/peerconfigs/.gitkeep b/testworld/peerconfigs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/testworld/peerconfigs/README.md b/testworld/peerconfigs/README.md new file mode 100644 index 000000000..ecbcd1263 --- /dev/null +++ b/testworld/peerconfigs/README.md @@ -0,0 +1 @@ +## FILES IN THIS DIRECTORY ARE GENERATED AUTOMATICALLY. THE GENERATED FILES ARE GIT IGNORED. DON'T PUT ANYTHING HERE THAT NEEDS TO BE COMMITED TO THE REPO. \ No newline at end of file diff --git a/testworld/proof_test.go b/testworld/proof_test.go new file mode 100644 index 000000000..c4f0882e2 --- /dev/null +++ b/testworld/proof_test.go @@ -0,0 +1,58 @@ +// +build testworld + +package testworld + +import ( + "net/http" + "testing" + + "github.com/gavv/httpexpect" +) + +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) { + alice := doctorFord.getHostTestSuite(t, "Alice") + bob := doctorFord.getHostTestSuite(t, "Bob") + + // Alice shares a document with Bob + res := createDocument(alice.httpExpect, documentType, http.StatusOK, defaultDocumentPayload(documentType, []string{bob.id.String()})) + + docIdentifier := getDocumentIdentifier(t, res) + if docIdentifier == "" { + t.Error("docIdentifier empty") + } + + proofPayload := defaultProofPayload(documentType) + + proofFromAlice := getProof(alice.httpExpect, http.StatusOK, docIdentifier, proofPayload) + proofFromBob := getProof(bob.httpExpect, http.StatusOK, docIdentifier, proofPayload) + + checkProof(proofFromAlice, documentType, docIdentifier) + checkProof(proofFromBob, documentType, docIdentifier) + +} + +func checkProof(objProof *httpexpect.Object, documentType string, docIdentifier string) { + + if documentType == typePO { + documentType = poPrefix + } + + objProof.Path("$.header.document_id").String().Equal(docIdentifier) + objProof.Path("$.field_proofs[0].property").String().Equal(documentType + ".net_amount") + objProof.Path("$.field_proofs[0].sorted_hashes").NotNull() + objProof.Path("$.field_proofs[1].property").String().Equal(documentType + ".currency") + objProof.Path("$.field_proofs[1].sorted_hashes").NotNull() + +} diff --git a/testworld/start_test.go b/testworld/start_test.go new file mode 100644 index 000000000..1f1b918b8 --- /dev/null +++ b/testworld/start_test.go @@ -0,0 +1,54 @@ +// This is the starting point for all Testworld tests +// +build testworld + +package testworld + +import ( + "fmt" + "os" + "testing" + + "github.com/centrifuge/go-centrifuge/config" +) + +var isRunningOnCI = len(os.Getenv("TRAVIS")) != 0 + +// Adjust these based on local testing requirments, please revert for CI server +var configFile = "configs/local.json" +var runPOAGeth = !isRunningOnCI + +// make this true this when running for the first time in local env +var createHostConfigs = isRunningOnCI + +// make this false if you want to make the tests run faster locally, but revert before committing to repo +var runMigrations = !isRunningOnCI + +// doctorFord manages the hosts +var doctorFord *hostManager + +func TestMain(m *testing.M) { + c, err := loadConfig(configFile) + if err != nil { + panic(err) + } + if runPOAGeth { + // NOTE that we don't bring down geth automatically right now because this must only be used for local testing purposes + startPOAGeth() + } + if runMigrations { + runSmartContractMigrations() + } + var contractAddresses *config.SmartContractAddresses + if c.Network == "testing" { + contractAddresses = getSmartContractAddresses() + } + doctorFord = newHostManager(c.EthNodeURL, c.AccountKeyPath, c.AccountPassword, c.Network, c.TxPoolAccess, contractAddresses) + err = doctorFord.init(createHostConfigs) + if err != nil { + panic(err) + } + fmt.Printf("contract addresses %+v\n", contractAddresses) + result := m.Run() + doctorFord.stop() + os.Exit(result) +} diff --git a/testworld/util.go b/testworld/util.go new file mode 100644 index 000000000..ea2f30d03 --- /dev/null +++ b/testworld/util.go @@ -0,0 +1,108 @@ +// +build testworld + +package testworld + +import ( + "io/ioutil" + "os" + "os/exec" + "path" + + "fmt" + + "github.com/centrifuge/go-centrifuge/config" + "github.com/savaki/jq" +) + +// startPOAGeth runs the proof of authority geth for tests +func startPOAGeth() { + // don't run if its already running + if IsPOAGethRunning() { + return + } + projDir := getProjectDir() + gethRunScript := path.Join(projDir, "build", "scripts", "docker", "run.sh") + o, err := exec.Command(gethRunScript, "dev").Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", string(o)) +} + +// runSmartContractMigrations migrates smart contracts to localgeth +func runSmartContractMigrations() { + projDir := getProjectDir() + migrationScript := path.Join(projDir, "build", "scripts", "migrate.sh") + _, err := exec.Command(migrationScript, projDir).Output() + if err != nil { + log.Fatal(err) + } +} + +// getSmartContractAddresses finds migrated smart contract addresses for localgeth +func getSmartContractAddresses() *config.SmartContractAddresses { + dat, err := findContractDeployJSON() + if err != nil { + panic(err) + } + idFactoryAddrOp := getOpForContract(".contracts.IdentityFactory.address") + idRegistryAddrOp := getOpForContract(".contracts.IdentityRegistry.address") + anchorRepoAddrOp := getOpForContract(".contracts.AnchorRepository.address") + payObAddrOp := getOpForContract(".contracts.PaymentObligation.address") + return &config.SmartContractAddresses{ + IdentityFactoryAddr: getOpAddr(idFactoryAddrOp, dat), + IdentityRegistryAddr: getOpAddr(idRegistryAddrOp, dat), + AnchorRepositoryAddr: getOpAddr(anchorRepoAddrOp, dat), + PaymentObligationAddr: getOpAddr(payObAddrOp, dat), + } +} + +func findContractDeployJSON() ([]byte, error) { + projDir := getProjectDir() + deployJSONFile := path.Join(projDir, "vendor", "github.com", "centrifuge", "centrifuge-ethereum-contracts", "deployments", "localgeth.json") + dat, err := ioutil.ReadFile(deployJSONFile) + if err != nil { + return nil, err + } + return dat, nil +} + +func getOpAddr(addrOp jq.Op, dat []byte) string { + addr, err := addrOp.Apply(dat) + if err != nil { + panic(err) + } + + // remove extra quotes inside the string + addrStr := string(addr) + if len(addrStr) > 0 && addrStr[0] == '"' { + addrStr = addrStr[1:] + } + if len(addrStr) > 0 && addrStr[len(addrStr)-1] == '"' { + addrStr = addrStr[:len(addrStr)-1] + } + return addrStr +} + +func getOpForContract(selector string) jq.Op { + addrOp, err := jq.Parse(selector) + if err != nil { + panic(err) + } + return addrOp +} + +func getProjectDir() string { + gp := os.Getenv("GOPATH") + projDir := path.Join(gp, "src", "github.com", "centrifuge", "go-centrifuge") + return projDir +} + +func IsPOAGethRunning() bool { + cmd := "docker ps -a --filter \"name=geth-node\" --filter \"status=running\" --quiet" + o, err := exec.Command("/bin/sh", "-c", cmd).Output() + if err != nil { + panic(err) + } + return len(o) != 0 +} diff --git a/utils/constants.go b/utils/constants.go index f4792fddb..c88f06370 100644 --- a/utils/constants.go +++ b/utils/constants.go @@ -1,5 +1,6 @@ package utils +// Constants that are common across packages. const ( PublicKey = "PUBLIC KEY" PrivateKey = "PRIVATE KEY" diff --git a/utils/events.go b/utils/events.go index f80610c30..f3ff9309b 100644 --- a/utils/events.go +++ b/utils/events.go @@ -1,9 +1,11 @@ package utils -import "fmt" +import ( + "github.com/centrifuge/go-centrifuge/errors" +) -// EventNotFound when event is not found and need to retry -var EventNotFound = fmt.Errorf("event not found") +// ErrEventNotFound when event is not found and need to retry +const ErrEventNotFound = errors.Error("event not found") // EventIterator contains functions that make events listening more easier type EventIterator interface { @@ -13,7 +15,7 @@ type EventIterator interface { } // LookForEvent checks if the iterator is ready with the Event -// if no event is found, returns EventNotFound +// if no event is found, returns ErrEventNotFound // returns iter.Error when iterator errored out func LookForEvent(iter EventIterator) (err error) { defer iter.Close() @@ -25,5 +27,5 @@ func LookForEvent(iter EventIterator) (err error) { return iter.Error() } - return EventNotFound + return ErrEventNotFound } diff --git a/utils/events_test.go b/utils/events_test.go index 6519b3efb..0bdf2e2bf 100644 --- a/utils/events_test.go +++ b/utils/events_test.go @@ -3,9 +3,9 @@ package utils import ( - "fmt" "testing" + "github.com/centrifuge/go-centrifuge/errors" "github.com/stretchr/testify/assert" ) @@ -27,7 +27,7 @@ func (m *mockIterator) Close() error { } func TestLookForEvent_iterator_error(t *testing.T) { - iter := &mockIterator{next: false, err: fmt.Errorf("failed iterator")} + iter := &mockIterator{next: false, err: errors.New("failed iterator")} err := LookForEvent(iter) assert.NotNil(t, err, "error should be non nil") assert.Contains(t, err.Error(), "failed iterator") @@ -37,7 +37,7 @@ func TestLookForEvent_event_not_found(t *testing.T) { iter := &mockIterator{} err := LookForEvent(iter) assert.NotNil(t, err, "error should be non nil") - assert.Equal(t, err, EventNotFound) + assert.Equal(t, err, ErrEventNotFound) } func TestLookForEvent_success(t *testing.T) { diff --git a/utils/httputils.go b/utils/httputils.go index bf47d42c3..7030ffbd7 100644 --- a/utils/httputils.go +++ b/utils/httputils.go @@ -1,13 +1,10 @@ package utils import ( - "github.com/go-errors/errors" - logging "github.com/ipfs/go-log" "gopkg.in/resty.v1" ) -var log = logging.Logger("http-utils") - +// SendPOSTRequest sends post with data to given URL. func SendPOSTRequest(url string, contentType string, payload []byte) (statusCode int, err error) { resp, err := resty.R(). SetHeader("Content-Type", contentType). @@ -15,12 +12,8 @@ func SendPOSTRequest(url string, contentType string, payload []byte) (statusCode Post(url) if err != nil { - log.Error(err) - return - } - if resp.StatusCode() != 200 { - err = errors.Errorf("%s", resp.Status()) + return statusCode, err } - statusCode = resp.StatusCode() - return + + return resp.StatusCode(), nil } diff --git a/utils/io.go b/utils/io.go index 667c7100c..e7e87d57c 100644 --- a/utils/io.go +++ b/utils/io.go @@ -2,9 +2,10 @@ package utils import ( "encoding/pem" - "fmt" "io/ioutil" "os" + + "github.com/centrifuge/go-centrifuge/errors" ) // WriteKeyToPemFile writes encode of key and purpose to the file @@ -33,10 +34,10 @@ func ReadKeyFromPemFile(fileName, keyPurpose string) (key []byte, err error) { } block, _ := pem.Decode(pemData) if block == nil { - return []byte{}, fmt.Errorf("file [%s] is not a valid pem file", fileName) + return []byte{}, errors.New("file [%s] is not a valid pem file", fileName) } if block.Type != keyPurpose { - return []byte{}, fmt.Errorf("key type mismatch got [%s] but expected [%s]", block.Type, keyPurpose) + return []byte{}, errors.New("key type mismatch got [%s] but expected [%s]", block.Type, keyPurpose) } return block.Bytes, nil diff --git a/utils/log_test.go b/utils/log_test.go index 2c804484d..1f3f399a6 100644 --- a/utils/log_test.go +++ b/utils/log_test.go @@ -1,3 +1,5 @@ +// +build unit + package utils import ( diff --git a/utils/time.go b/utils/time.go index 334f74e1f..1338b04c9 100644 --- a/utils/time.go +++ b/utils/time.go @@ -6,6 +6,7 @@ import ( "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()), diff --git a/utils/tools.go b/utils/tools.go index cd3403651..530004747 100644 --- a/utils/tools.go +++ b/utils/tools.go @@ -2,14 +2,16 @@ package utils import ( "crypto/rand" - "errors" "math/big" + "github.com/centrifuge/go-centrifuge/errors" + "github.com/centrifuge/gocelery" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) +// ContainsBigIntInSlice checks if value is present in list. func ContainsBigIntInSlice(value *big.Int, list []*big.Int) bool { for _, v := range list { if v.Cmp(value) == 0 { @@ -25,10 +27,10 @@ func SliceToByte32(in []byte) (out [32]byte, err error) { return [32]byte{}, errors.New("input exceeds length of 32") } copy(out[:], in) - return + return out, nil } -// +// IsEmptyAddress checks if the addr is empty. func IsEmptyAddress(addr common.Address) bool { return addr.Hex() == "0x0000000000000000000000000000000000000000" } @@ -46,12 +48,12 @@ func SliceOfByteSlicesToHexStringSlice(byteSlices [][]byte) []string { func Byte32ToSlice(in [32]byte) []byte { if IsEmptyByte32(in) { return []byte{} - } else { - return in[:] } + + return in[:] } -// Check32BytesFilled takes multiple []byte slices and ensures they are all of length 32 and don't contain all 0x0 bytes. +// CheckMultiple32BytesFilled takes multiple []byte slices and ensures they are all of length 32 and don't contain all 0x0 bytes. func CheckMultiple32BytesFilled(b []byte, bs ...[]byte) bool { bs = append(bs, b) for _, v := range bs { @@ -80,6 +82,7 @@ func RandomByte32() (out [32]byte) { return } +// IsEmptyByte32 checks if the source is empty. func IsEmptyByte32(source [32]byte) bool { sl := make([]byte, 32) copy(sl, source[:32]) @@ -102,6 +105,7 @@ func IsEmptyByteSlice(s []byte) bool { return true } +// IsSameByteSlice checks if a and b contains same bytes. func IsSameByteSlice(a []byte, b []byte) bool { if a == nil && b == nil { return true @@ -124,7 +128,7 @@ func IsSameByteSlice(a []byte, b []byte) bool { return true } -// ByteFixedToBigInt convert bute slices to big.Int (bigendian) +// ByteSliceToBigInt convert bute slices to big.Int (bigendian) func ByteSliceToBigInt(slice []byte) *big.Int { bi := new(big.Int) bi.SetBytes(slice) @@ -138,17 +142,19 @@ func ByteFixedToBigInt(bytes []byte, size int) *big.Int { return bi } -// Useful for tests -func SimulateJsonDecodeForGocelery(kwargs map[string]interface{}) (map[string]interface{}, error) { +// SimulateJSONDecodeForGocelery encodes and decodes the kwargs. +func SimulateJSONDecodeForGocelery(kwargs map[string]interface{}) (map[string]interface{}, error) { t1 := gocelery.TaskMessage{Kwargs: kwargs} encoded, err := t1.Encode() if err != nil { return nil, err } + t2, err := gocelery.DecodeTaskMessage(encoded) return t2.Kwargs, err } +// IsValidByteSliceForLength checks if the len(slice) == length. func IsValidByteSliceForLength(slice []byte, length int) bool { return len(slice) == length } diff --git a/version/bootstrapper.go b/version/bootstrapper.go index 33df94c42..33c111146 100644 --- a/version/bootstrapper.go +++ b/version/bootstrapper.go @@ -1,8 +1,9 @@ package version -type Bootstrapper struct { -} +// Bootstrapper implements bootstrapper.Bootstrapper +type Bootstrapper struct{} +// Bootstrap logs the cent node version. func (*Bootstrapper) Bootstrap(context map[string]interface{}) error { log.Infof("Running cent node on version: %s", GetVersion()) return nil diff --git a/version/check.go b/version/check.go index 8424d98f7..dda535bd5 100644 --- a/version/check.go +++ b/version/check.go @@ -21,7 +21,7 @@ func checkMajorCompatibility(versionString string) (match bool, err error) { return v.Major() == GetVersion().Major(), nil } -// checkVersion checks if the peer node version matches with the current node +// CheckVersion checks if the peer node version matches with the current node. func CheckVersion(peerVersion string) bool { compatible, err := checkMajorCompatibility(peerVersion) if err != nil { diff --git a/version/version.go b/version/version.go index 3bc2c7479..e171338ef 100644 --- a/version/version.go +++ b/version/version.go @@ -12,13 +12,12 @@ var gitCommit = "master" // CentrifugeNodeVersion is the current version of the app const CentrifugeNodeVersion = "0.0.1-alpha" +// GetVersion returns current cent node version in semvar format. func GetVersion() *semver.Version { - v, err := semver.NewVersion(fmt.Sprintf("%s+%s", CentrifugeNodeVersion, gitCommit)) if err != nil { log.Panicf("Invalid CentrifugeNodeVersion specified: %s", CentrifugeNodeVersion) } return v - }