From 22db299b7b5334ccf5584b6df1680a5122f6dc44 Mon Sep 17 00:00:00 2001 From: Uk Date: Tue, 6 Jul 2021 19:07:51 +0545 Subject: [PATCH 1/4] Merge --- .github/workflows/build.yml | 86 ++++ .github/workflows/ci.yml | 3 +- .github/workflows/cicd.yml | 108 +++++ .github/workflows/cicd_hetz.yml | 166 ++++++++ .github/workflows/test.yml | 10 + README.md | 7 + code/go/0chain.net/blobber/main.go | 388 +++++++++--------- .../blobbercore/allocation/entity.go | 16 +- .../blobbercore/allocation/newfilechange.go | 9 + .../blobbercore/allocation/protocol.go | 3 +- .../blobbercore/blobbergrpc/blobber.pb.go | 98 +++-- .../blobbergrpc/proto/blobber.proto | 23 +- .../blobbercore/challenge/entity.go | 1 + .../blobbercore/challenge/worker.go | 7 +- .../0chain.net/blobbercore/config/config.go | 18 + .../blobbercore/filestore/chunk_writer.go | 115 ++++++ .../filestore/chunk_writer_test.go | 93 +++++ .../blobbercore/filestore/fs_store.go | 63 ++- .../0chain.net/blobbercore/filestore/store.go | 14 + .../0chain.net/blobbercore/handler/convert.go | 3 +- code/go/0chain.net/blobbercore/handler/dto.go | 5 + .../blobbercore/handler/grpc_handler.go | 92 ++--- .../0chain.net/blobbercore/handler/helper.go | 9 +- .../handler/object_operation_handler.go | 141 ++++--- .../blobbercore/handler/protocol.go | 136 +++--- .../blobbercore/handler/storage_handler.go | 36 +- .../0chain.net/blobbercore/handler/zcncore.go | 40 +- .../blobbercore/openapi/blobber.swagger.json | 9 +- .../blobbercore/readmarker/entity.go | 1 - .../blobbercore/stats/blobberstats.go | 11 +- .../0chain.net/blobbercore/stats/handler.go | 2 +- code/go/0chain.net/core/common/handler.go | 2 + code/go/0chain.net/core/encryption/keys.go | 59 +++ .../0chain.net/core/encryption/keys_test.go | 56 +++ code/go/0chain.net/core/transaction/entity.go | 20 +- code/go/0chain.net/go.mod | 1 + .../validatorcore/storage/context.go | 3 +- config/0chain_blobber.yaml | 4 + docker.local/b0docker-compose.yml | 2 +- .../bin/build.blobber-integration-tests.sh | 17 +- docker.local/bin/build.blobber.sh | 17 +- docs/cicd/CICD_GITACTIONS.md | 128 ++++++ docs/cicd/blobber.png | Bin 0 -> 54031 bytes docs/src/repair.plantuml | 2 +- sql/14-increase_owner_pubkey.sql | 16 + sql/15-add-allocation-columns.sql | 6 + 46 files changed, 1541 insertions(+), 505 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cicd.yml create mode 100644 .github/workflows/cicd_hetz.yml create mode 100644 .github/workflows/test.yml create mode 100644 code/go/0chain.net/blobbercore/filestore/chunk_writer.go create mode 100644 code/go/0chain.net/blobbercore/filestore/chunk_writer_test.go create mode 100644 docs/cicd/CICD_GITACTIONS.md create mode 100644 docs/cicd/blobber.png create mode 100644 sql/14-increase_owner_pubkey.sql create mode 100644 sql/15-add-allocation-columns.sql diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..c9ceeb49e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,86 @@ +name: Dockerize + +on: + workflow_dispatch: + inputs: + latest_tag: + description: 'type yes for building latest tag' + default: 'no' + required: true + +env: + ZCHAIN_BUILDBASE: zchain_build_base + ZCHAIN_BUILDRUN: zchain_run_base + BLOBBER_REGISTRY: ${{ secrets.BLOBBER_REGISTRY }} + VALIDATOR_REGISTRY: ${{ secrets.VALIDATOR_REGISTRY }} + +jobs: + dockerize_blobber: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Get the version + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build blobber + run: | + docker build -t $BLOBBER_REGISTRY:$TAG -f "$DOCKERFILE_BLOB" . + docker tag $BLOBBER_REGISTRY:$TAG $BLOBBER_REGISTRY:latest + docker push $BLOBBER_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_BLOB: "docker.local/Dockerfile" + + - name: Push blobber + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $BLOBBER_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} + + dockerize_validator: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v1 + + - name: Get the version + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build validator + run: | + docker build -t $VALIDATOR_REGISTRY:$TAG -f "$DOCKERFILE_PROXY" . + docker tag $VALIDATOR_REGISTRY:$TAG $VALIDATOR_REGISTRY:latest + docker push $VALIDATOR_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_PROXY: "docker.local/ValidatorDockerfile" + + - name: Push validator + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $VALIDATOR_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fc27a3d3..8b53fa32f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: run: | docker network create --driver=bridge --subnet=198.18.0.0/15 --gateway=198.18.0.255 testnet0 ./docker.local/bin/build.blobber.sh - test: runs-on: ubuntu-20.04 steps: @@ -128,4 +127,4 @@ jobs: docker build -t $VALIDATOR_REGISTRY:$TAG -f docker.local/ValidatorDockerfile . docker push $VALIDATOR_REGISTRY:$TAG env: - TAG: ${{ steps.get_version.outputs.VERSION }} + TAG: ${{ steps.get_version.outputs.VERSION }} \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 000000000..abb166577 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,108 @@ +name: CICD_TEST_HERTZNER + +on: + push: + branches: + - gitactionsfix + +env: + GITHUB_TOKEN: ${{ secrets.CICD }} + +jobs: + build-push-image: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Get Branch + id: get_branch + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + echo ::set-output name=BRANCH::${BRANCH} + echo "This workflow run is triggered by ${{ github.event_name }} ." + + - name: Triggering build.yml for creating & pushing docker images. + uses: convictional/trigger-workflow-and-wait@v1.3.0 + with: + owner: 0chain + repo: blobber + github_token: ${{ secrets.CICD }} + workflow_file_name: test.yml + ref: ${{ steps.get_branch.outputs.BRANCH }} + # inputs: '{"DOCKERHUB_REPO":"TEST"}' + propagate_failure: true + trigger_workflow: true + wait_workflow: true + + # network-setup: + # runs-on: ubuntu-20.04 + # env: + # HOST: testnet.load.testnet-0chain.net + # steps: + # - uses: actions/checkout@v2 + + # - uses: azure/setup-helm@v1 + # with: + # version: 'v3.2.2' + # - name: Setup helm repo + # run: | + # helm repo add 0chain http://0chain-helm-chart.s3-website.us-east-2.amazonaws.com/ + # - name: Setup kubeconfig + # run: | + # mkdir -p ~/.kube + # echo "${{ secrets.KUBECONFIG64TEST }}" | base64 -d > ~/.kube/config + # - name: Uninstall old release + # run: | + # helm uninstall zerochain -n zerochain || true + # - name: Setup chain + # run: | + # helm install zerochain -n zerochain \ + # --set ingress.host=${HOST} \ + # --set sharder.image.tag=latest \ + # --set miner.image.tag=latest \ + # --set blobber.image.tag=latest \ + # --set validator.image.tag=latest \ + # 0chain/0chain + # - name: Check if services are up + # run: | + # printf 'Waiting for 0dns' + # until [[ $(curl -I --silent -o /dev/null -w %{http_code} http://${HOST}/dns/) =~ 2[0-9][0-9] ]] ;do + # printf '.' + # sleep 2 + # done + # printf 'Waiting for 1st sharder' + # until [[ $(curl -I --silent -o /dev/null -w %{http_code} http://${HOST}/sharder0/) =~ 2[0-9][0-9] ]] ;do + # printf '.' + # sleep 2 + # done + # printf 'Waiting for 1st miner' + # until [[ $(curl -I --silent -o /dev/null -w %{http_code} http://${HOST}/miner0/) =~ 2[0-9][0-9] ]] ;do + # printf '.' + # sleep 2 + # done + + # # - name: Triggering ci.yml to run Postman API tests + # # uses: convictional/trigger-workflow-and-wait@v1.3.0 + # # with: + # # owner: 0chain + # # repo: 0proxy + # # github_token: ${{ secrets.GOSDK }} + # # workflow_file_name: build.yml + # # ref: master + # # propagate_failure: true + # # trigger_workflow: true + # # wait_workflow: true + + # # - name: Build + # # run: make build + # # - name: Running Load Tests + # # run: | + # # make run config=loadTest-load-testnet.yaml + + # - name: Uninstall the release + # if: ${{ always() }} + # run: helm uninstall zerochain -n zerochain + + # - name: Deleting kubeconfig + # run: | + # rm -rf ~/.kube diff --git a/.github/workflows/cicd_hetz.yml b/.github/workflows/cicd_hetz.yml new file mode 100644 index 000000000..47a16e435 --- /dev/null +++ b/.github/workflows/cicd_hetz.yml @@ -0,0 +1,166 @@ +name: CICD_Hetzner + +# on: +# push: +# branches: +# - gitactionsfix + +on: + workflow_dispatch: + inputs: + latest_tag: + description: 'type yes for building latest tag' + default: 'no' + required: true + +env: + ZCHAIN_BUILDBASE: zchain_build_base + ZCHAIN_BUILDRUN: zchain_run_base + BLOBBER_REGISTRY: ${{ secrets.BLOBBER_REGISTRY_TEST }} + VALIDATOR_REGISTRY: ${{ secrets.VALIDATOR_REGISTRY_TEST }} + KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA_TEST }} + KUBE_NAMESPACE: test + +jobs: + dockerize_blobber: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Get the Version for Tagging + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build & Push Blobber Image + run: | + docker build -t $BLOBBER_REGISTRY:$TAG -f "$DOCKERFILE_BLOB" . + docker tag $BLOBBER_REGISTRY:$TAG $BLOBBER_REGISTRY:latest + # docker push $BLOBBER_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_BLOB: "docker.local/Dockerfile" + + - name: Push Blobber Image with Latest TAG + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $BLOBBER_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} + + - name: Update Blobber Image to Kubernetes Cluster + uses: kodermax/kubectl-aws-eks@master + with: + args: set image deployment/blobber-01 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/blobber-02 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/blobber-03 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/blobber-04 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/blobber-05 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/blobber-06 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + + - name: Verify Kubernetes Deployment + uses: kodermax/kubectl-aws-eks@master + with: + args: rollout status blobber-01 my-pod -n $KUBE_NAMESPACE + args: rollout status blobber-02 my-pod -n $KUBE_NAMESPACE + args: rollout status blobber-03 my-pod -n $KUBE_NAMESPACE + args: rollout status blobber-04 my-pod -n $KUBE_NAMESPACE + args: rollout status blobber-05 my-pod -n $KUBE_NAMESPACE + args: rollout status blobber-06 my-pod -n $KUBE_NAMESPACE + + # - name: Triggering LoadTest Repo build + # uses: convictional/trigger-workflow-and-wait@v1.3.0 + # with: + # owner: 0chain + # repo: loadTest + # github_token: ${{ secrets.GOSDK }} + # workflow_file_name: load-test-v1.yml + # ref: master + # inputs: '{}' + # propagate_failure: true + # trigger_workflow: true + # wait_workflow: true + + + dockerize_validator: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v1 + + - name: Get the Version for Tagging + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build & Push Validator Image + run: | + docker build -t $VALIDATOR_REGISTRY:$TAG -f "$DOCKERFILE_PROXY" . + docker tag $VALIDATOR_REGISTRY:$TAG $VALIDATOR_REGISTRY:latest + # docker push $VALIDATOR_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_PROXY: "docker.local/ValidatorDockerfile" + + - name: Push Validator Image with Latest TAG + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $VALIDATOR_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} + + - name: Update Validator Image to Kubernetes Cluster + uses: kodermax/kubectl-aws-eks@master + with: + args: set image deployment/validator-01 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/validator-02 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/validator-03 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/validator-04 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/validator-05 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + args: set image deployment/validator-06 app=$BLOBBER_REGISTRY:$TAG --record -n $KUBE_NAMESPACE + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + + - name: Verify Kubernetes Deployment + uses: kodermax/kubectl-aws-eks@master + with: + args: rollout status validator-01 -n $KUBE_NAMESPACE + args: rollout status validator-02 -n $KUBE_NAMESPACE + args: rollout status validator-03 -n $KUBE_NAMESPACE + args: rollout status validator-04 -n $KUBE_NAMESPACE + args: rollout status validator-05 -n $KUBE_NAMESPACE + args: rollout status validator-06 -n $KUBE_NAMESPACE + + # - name: Triggering LoadTest Repo build + # uses: convictional/trigger-workflow-and-wait@v1.3.0 + # with: + # owner: 0chain + # repo: loadTest + # github_token: ${{ secrets.GOSDK }} + # workflow_file_name: load-test-v1.yml + # ref: master + # inputs: '{}' + # propagate_failure: true + # trigger_workflow: true + # wait_workflow: true \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..bb984e3a6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,10 @@ +name: test + +on: + workflow_dispatch + +jobs: + dockerize_blobber: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 diff --git a/README.md b/README.md index c1630c531..f55f8a205 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,13 @@ To link to local gosdk so that the changes are reflected on the blobber build pl $ ./docker.local/bin/build.blobber.dev.sh ``` +For Apple M1 chip builds: + +``` + +$ ./docker.local/bin/build.blobber.sh -m1 + +``` 3. After building the container for blobber, go to Blobber1 directory (git/blobber/docker.local/blobber1) and run the container using diff --git a/code/go/0chain.net/blobber/main.go b/code/go/0chain.net/blobber/main.go index 322c27232..2a1944721 100644 --- a/code/go/0chain.net/blobber/main.go +++ b/code/go/0chain.net/blobber/main.go @@ -8,13 +8,16 @@ import ( "log" "net" "net/http" - "net/url" "os" "runtime" "strconv" - "strings" "time" + "go.uber.org/zap" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/spf13/viper" + "github.com/0chain/gosdk/zcncore" "0chain.net/blobbercore/allocation" "0chain.net/blobbercore/challenge" "0chain.net/blobbercore/config" @@ -27,32 +30,50 @@ import ( "0chain.net/core/chain" "0chain.net/core/common" "0chain.net/core/encryption" + "0chain.net/core/node" "0chain.net/core/logging" . "0chain.net/core/logging" - "0chain.net/core/node" - "0chain.net/core/transaction" - "0chain.net/core/util" - - "github.com/0chain/gosdk/zcncore" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/spf13/viper" - "go.uber.org/zap" ) -//var BLOBBER_REGISTERED_LOOKUP_KEY = datastore.ToKey("blobber_registration") - var startTime time.Time var serverChain *chain.Chain var filesDir *string var metadataDB *string func initHandlers(r *mux.Router) { - r.HandleFunc("/", HomePageHandler) + r.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { + mc := chain.GetServerChain() + + fmt.Fprintf(w, "
Running since %v ...\n", startTime) + fmt.Fprintf(w, "
Working on the chain: %v
\n", mc.ID) + fmt.Fprintf(w, + "
I am a blobber with
  • id:%v
  • public_key:%v
  • build_tag:%v
\n", + node.Self.ID, node.Self.PublicKey, build.BuildTag, + ) + + fmt.Fprintf(w, "
Miners ...\n") + network := zcncore.GetNetwork() + for _, miner := range network.Miners { + fmt.Fprintf(w, "%v\n", miner) + } + + fmt.Fprintf(w, "
Sharders ...\n") + for _, sharder := range network.Sharders { + fmt.Fprintf(w, "%v\n", sharder) + } + }) + handler.SetupHandlers(r) } -func SetupWorkerConfig() { +var fsStore filestore.FileStore //nolint:unused // global which might be needed somewhere + +func initEntities() (err error) { + fsStore, err = filestore.SetupFSStore(*filesDir + "/files") + return err +} + +func setupWorkerConfig() { config.Configuration.ContentRefWorkerFreq = viper.GetInt64("contentref_cleaner.frequency") config.Configuration.ContentRefWorkerTolerance = viper.GetInt64("contentref_cleaner.tolerance") @@ -117,48 +138,7 @@ func SetupWorkerConfig() { config.Configuration.ServiceCharge = viper.GetFloat64("service_charge") } -func SetupWorkers() { - var root = common.GetRootContext() - handler.SetupWorkers(root) - challenge.SetupWorkers(root) - readmarker.SetupWorkers(root) - writemarker.SetupWorkers(root) - allocation.StartUpdateWorker(root, - config.Configuration.UpdateAllocationsInterval) - // stats.StartEventDispatcher(2) -} - -var fsStore filestore.FileStore //nolint:unused // global which might be needed somewhere - -func initEntities() (err error) { - fsStore, err = filestore.SetupFSStore(*filesDir + "/files") - return err -} - -func initServer() { - -} - -func checkForDBConnection() { - retries := 0 - var err error - for retries < 600 { - err = datastore.GetStore().Open() - if err != nil { - time.Sleep(1 * time.Second) - retries++ - continue - } - break - } - - if err != nil { - Logger.Error("Error in opening the database. Shutting the server down") - panic(err) - } -} - -func processMinioConfig(reader io.Reader) error { +func setupMinioConfig(reader io.Reader) error { scanner := bufio.NewScanner(reader) more := scanner.Scan() if !more { @@ -193,26 +173,154 @@ func processMinioConfig(reader io.Reader) error { return nil } -func isValidOrigin(origin string) bool { - var url, err = url.Parse(origin) +func setupWorkers() { + var root = common.GetRootContext() + handler.SetupWorkers(root) + challenge.SetupWorkers(root) + readmarker.SetupWorkers(root) + writemarker.SetupWorkers(root) + allocation.StartUpdateWorker(root, + config.Configuration.UpdateAllocationsInterval) +} + +func setupDatabase() { + // check for database connection + for i := 600; i > 0; i-- { + time.Sleep(1 * time.Second) + if err := datastore.GetStore().Open(); err == nil { + if i == 1 { // no more attempts + Logger.Error("Failed to connect to the database. Shutting the server down") + panic(err) // fail + } + + return // success + } + } +} + +func setupOnChain() { + const ATTEMPT_DELAY = 60 * 1 // 1 minute + + // setup wallet + if err := handler.WalletRegister(); err != nil { + panic(err) + } + + // setup blobber (add or update) on the blockchain (multiple attempts) + for i := 10; i > 0; i-- { + if err := addOrUpdateOnChain(); err != nil { + if i == 1 { // no more attempts + panic(err) + } + } else { + break + } + + time.Sleep(ATTEMPT_DELAY * time.Second) + } + + setupWorkers() + + go healthCheckOnChainWorker() + + if config.Configuration.PriceInUSD { + go addOrUpdateOnChainWorker() + } +} + +func addOrUpdateOnChain() error { + txnHash, err := handler.BlobberAdd(common.GetRootContext()) + if err != nil { + return err + } + + if t, err := handler.TransactionVerify(txnHash); err != nil { + Logger.Error("Failed to verify blobber add/update transaction", zap.Any("err", err), zap.String("txn.Hash", txnHash)) + } else { + Logger.Info("Verified blobber add/update transaction", zap.String("txn_hash", t.Hash), zap.Any("txn_output", t.TransactionOutput)) + } + + return err +} + +func addOrUpdateOnChainWorker() { + var REPEAT_DELAY = 60 * 60 * time.Duration(viper.GetInt("price_worker_in_hours")) // 12 hours with default settings + for { + time.Sleep(REPEAT_DELAY * time.Second) + if err := addOrUpdateOnChain(); err != nil { + continue // pass // required by linting + } + } +} + +func healthCheckOnChain() error { + txnHash, err := handler.BlobberHealthCheck(common.GetRootContext()) if err != nil { - return false + if err == handler.ErrBlobberHasRemoved { + return nil + } else { + return err + } + } + + if t, err := handler.TransactionVerify(txnHash); err != nil { + Logger.Error("Failed to verify blobber health check", zap.Any("err", err), zap.String("txn.Hash", txnHash)) + } else { + Logger.Info("Verified blobber health check", zap.String("txn_hash", t.Hash), zap.Any("txn_output", t.TransactionOutput)) + } + + return err +} + +func healthCheckOnChainWorker() { + const REPEAT_DELAY = 60 * 15 // 15 minutes + + for { + time.Sleep(REPEAT_DELAY * time.Second) + if err := healthCheckOnChain(); err != nil { + continue // pass // required by linting + } } - var host = url.Hostname() - if host == "localhost" { - return true +} + +func setup(logDir string) error { + // init blockchain related stuff + zcncore.SetLogFile(logDir + "/0chainBlobber.log", false) + zcncore.SetLogLevel(3) + if err := zcncore.InitZCNSDK(serverChain.BlockWorker, config.Configuration.SignatureScheme); err != nil { + return err } - if host == "0chain.net" || host == "0box.io" || - strings.HasSuffix(host, ".0chain.net") || - strings.HasSuffix(host, ".alphanet-0chain.net") || - strings.HasSuffix(host, ".testnet-0chain.net") || - strings.HasSuffix(host, ".devnet-0chain.net") || - strings.HasSuffix(host, ".mainnet-0chain.net") { - return true + if err := zcncore.SetWalletInfo(node.Self.GetWalletString(), false); err != nil { + return err } - return false + + // setup on blockchain + go setupOnChain() + return nil } +// // Comment out to pass lint. Still keep this function around in case we want to +// // change how CORS validates origins. +// func isValidOrigin(origin string) bool { +// var url, err = url.Parse(origin) +// if err != nil { +// return false +// } +// var host = url.Hostname() +// if host == "localhost" { +// return true +// } +// if host == "0chain.net" || host == "0box.io" || +// strings.HasSuffix(host, ".0chain.net") || +// strings.HasSuffix(host, ".alphanet-0chain.net") || +// strings.HasSuffix(host, ".testnet-0chain.net") || +// strings.HasSuffix(host, ".devnet-0chain.net") || +// strings.HasSuffix(host, ".mainnet-0chain.net") { +// return true +// } +// return false +// } + func main() { deploymentMode := flag.Int("deployment_mode", 2, "deployment_mode") keysFile := flag.String("keys_file", "", "keys_file") @@ -238,7 +346,7 @@ func main() { } config.Configuration.ChainID = viper.GetString("server_chain.id") config.Configuration.SignatureScheme = viper.GetString("server_chain.signature_scheme") - SetupWorkerConfig() + setupWorkerConfig() if *filesDir == "" { panic("Please specify --files_dir absolute folder name option where uploaded files can be stored") @@ -273,7 +381,7 @@ func main() { panic(err) } - err = processMinioConfig(reader) + err = setupMinioConfig(reader) if err != nil { panic(err) } @@ -309,13 +417,13 @@ func main() { chain.SetServerChain(serverChain) - checkForDBConnection() + setupDatabase() // Initialize after server chain is setup. if err := initEntities(); err != nil { Logger.Error("Error setting up blobber on blockchian" + err.Error()) } - if err := SetupBlobberOnBC(*logDir); err != nil { + if err := setup(*logDir); err != nil { Logger.Error("Error setting up blobber on blockchian" + err.Error()) } mode := "main net" @@ -334,14 +442,18 @@ func main() { headersOk := handlers.AllowedHeaders([]string{ "X-Requested-With", "X-App-Client-ID", "X-App-Client-Key", "Content-Type", + "X-App-Client-Signature", }) - originsOk := handlers.AllowedOriginValidator(isValidOrigin) + + // Allow anybody to access API. + // originsOk := handlers.AllowedOriginValidator(isValidOrigin) + originsOk := handlers.AllowedOrigins([]string{"*"}) + methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) rl := common.ConfigRateLimits() initHandlers(r) - initServer() grpcServer := handler.NewServerWithMiddlewares(rl) handler.RegisterGRPCServices(r, grpcServer) @@ -379,125 +491,3 @@ func main() { }(*grpcPortString) log.Fatal(server.ListenAndServe()) } - -func RegisterBlobber() { - - registrationRetries := 0 - //ctx := badgerdbstore.GetStorageProvider().WithConnection(common.GetRootContext()) - for registrationRetries < 10 { - txnHash, err := handler.RegisterBlobber(common.GetRootContext()) - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - txnVerified := false - verifyRetries := 0 - for verifyRetries < util.MAX_RETRIES { - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - t, err := transaction.VerifyTransaction(txnHash, chain.GetServerChain()) - if err == nil { - Logger.Info("Transaction for adding blobber accepted and verified", zap.String("txn_hash", t.Hash), zap.Any("txn_output", t.TransactionOutput)) - //badgerdbstore.GetStorageProvider().WriteBytes(ctx, BLOBBER_REGISTERED_LOOKUP_KEY, []byte(txnHash)) - //badgerdbstore.GetStorageProvider().Commit(ctx) - SetupWorkers() - go BlobberHealthCheck() - if config.Configuration.PriceInUSD { - go UpdateBlobberSettings() - } - return - } - verifyRetries++ - } - - if !txnVerified { - Logger.Error("Add blobber transaction could not be verified", zap.Any("err", err), zap.String("txn.Hash", txnHash)) - } - } -} - -func BlobberHealthCheck() { - const HEALTH_CHECK_TIMER = 60 * 15 // 15 Minutes - for { - txnHash, err := handler.BlobberHealthCheck(common.GetRootContext()) - if err != nil && err == handler.ErrBlobberHasRemoved { - time.Sleep(HEALTH_CHECK_TIMER * time.Second) - continue - } - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - txnVerified := false - verifyRetries := 0 - for verifyRetries < util.MAX_RETRIES { - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - t, err := transaction.VerifyTransaction(txnHash, chain.GetServerChain()) - if err == nil { - txnVerified = true - Logger.Info("Transaction for blobber health check verified", zap.String("txn_hash", t.Hash), zap.Any("txn_output", t.TransactionOutput)) - break - } - verifyRetries++ - } - - if !txnVerified { - Logger.Error("Blobber health check transaction could not be verified", zap.Any("err", err), zap.String("txn.Hash", txnHash)) - } - time.Sleep(HEALTH_CHECK_TIMER * time.Second) - } -} - -func UpdateBlobberSettings() { - var UPDATE_SETTINGS_TIMER = 60 * 60 * time.Duration(viper.GetInt("price_worker_in_hours")) - time.Sleep(UPDATE_SETTINGS_TIMER * time.Second) - for { - txnHash, err := handler.UpdateBlobberSettings(common.GetRootContext()) - if err != nil { - time.Sleep(UPDATE_SETTINGS_TIMER * time.Second) - continue - } - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - txnVerified := false - verifyRetries := 0 - for verifyRetries < util.MAX_RETRIES { - time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - t, err := transaction.VerifyTransaction(txnHash, chain.GetServerChain()) - if err == nil { - txnVerified = true - Logger.Info("Transaction for blobber update settings verified", zap.String("txn_hash", t.Hash), zap.Any("txn_output", t.TransactionOutput)) - break - } - verifyRetries++ - } - - if !txnVerified { - Logger.Error("Blobber update settings transaction could not be verified", zap.Any("err", err), zap.String("txn.Hash", txnHash)) - } - time.Sleep(UPDATE_SETTINGS_TIMER * time.Second) - } -} - -func SetupBlobberOnBC(logDir string) error { - var logName = logDir + "/0chainBlobber.log" - zcncore.SetLogFile(logName, false) - zcncore.SetLogLevel(3) - if err := zcncore.InitZCNSDK(serverChain.BlockWorker, config.Configuration.SignatureScheme); err != nil { - return err - } - if err := zcncore.SetWalletInfo(node.Self.GetWalletString(), false); err != nil { - return err - } - go RegisterBlobber() - return nil -} - -/*HomePageHandler - provides basic info when accessing the home page of the server */ -func HomePageHandler(w http.ResponseWriter, r *http.Request) { - mc := chain.GetServerChain() - fmt.Fprintf(w, "
Running since %v ...\n", startTime) - fmt.Fprintf(w, "
Working on the chain: %v
\n", mc.ID) - fmt.Fprintf(w, "
I am a blobber with
  • id:%v
  • public_key:%v
  • build_tag:%v
\n", node.Self.ID, node.Self.PublicKey, build.BuildTag) - fmt.Fprintf(w, "
Miners ...\n") - network := zcncore.GetNetwork() - for _, miner := range network.Miners { - fmt.Fprintf(w, "%v\n", miner) - } - fmt.Fprintf(w, "
Sharders ...\n") - for _, sharder := range network.Sharders { - fmt.Fprintf(w, "%v\n", sharder) - } -} diff --git a/code/go/0chain.net/blobbercore/allocation/entity.go b/code/go/0chain.net/blobbercore/allocation/entity.go index 62a11125a..3070b4d8d 100644 --- a/code/go/0chain.net/blobbercore/allocation/entity.go +++ b/code/go/0chain.net/blobbercore/allocation/entity.go @@ -25,6 +25,8 @@ type Allocation struct { UsedSize int64 `gorm:"column:used_size"` OwnerID string `gorm:"column:owner_id"` OwnerPublicKey string `gorm:"column:owner_public_key"` + RepairerID string `gorm:"column:repairer_id"`// experimental / blobber node id + PayerID string `gorm:"column:payer_id"` // optional / client paying for all r/w ops Expiration common.Timestamp `gorm:"column:expiration_date"` AllocationRoot string `gorm:"column:allocation_root"` BlobberSize int64 `gorm:"column:blobber_size"` @@ -32,14 +34,12 @@ type Allocation struct { LatestRedeemedWM string `gorm:"column:latest_redeemed_write_marker"` IsRedeemRequired bool `gorm:"column:is_redeem_required"` TimeUnit time.Duration `gorm:"column:time_unit"` - // ending and cleaning - CleanedUp bool `gorm:"column:cleaned_up"` - Finalized bool `gorm:"column:finalized"` - // Has many terms. - Terms []*Terms `gorm:"-"` - - // Used for 3rd party/payer operations - PayerID string `gorm:"column:payer_id"` + IsImmutable bool `gorm:"is_immutable"` + // Ending and cleaning + CleanedUp bool `gorm:"column:cleaned_up"` + Finalized bool `gorm:"column:finalized"` + // Has many terms + Terms []*Terms `gorm:"-"` } func (Allocation) TableName() string { diff --git a/code/go/0chain.net/blobbercore/allocation/newfilechange.go b/code/go/0chain.net/blobbercore/allocation/newfilechange.go index 280e2cd6d..346ea89fb 100644 --- a/code/go/0chain.net/blobbercore/allocation/newfilechange.go +++ b/code/go/0chain.net/blobbercore/allocation/newfilechange.go @@ -32,6 +32,15 @@ type NewFileChange struct { EncryptedKey string `json:"encrypted_key,omitempty"` CustomMeta string `json:"custom_meta,omitempty"` Attributes reference.Attributes `json:"attributes,omitempty"` + + // IsResumable the request is resumable upload + IsResumable bool `json:"is_resumable,omitempty"` + // UploadLength indicates the size of the entire upload in bytes. The value MUST be a non-negative integer. + UploadLength int64 `json:"upload_length,omitempty"` + // Upload-Offset indicates a byte offset within a resource. The value MUST be a non-negative integer. + UploadOffset int64 `json:"upload_offset,omitempty"` + // IsFinal the request is final chunk + IsFinal bool `json:"is_final,omitempty"` } func (nf *NewFileChange) ProcessChange(ctx context.Context, diff --git a/code/go/0chain.net/blobbercore/allocation/protocol.go b/code/go/0chain.net/blobbercore/allocation/protocol.go index 01c07bf26..9239ef701 100644 --- a/code/go/0chain.net/blobbercore/allocation/protocol.go +++ b/code/go/0chain.net/blobbercore/allocation/protocol.go @@ -122,11 +122,12 @@ func VerifyAllocationTransaction(ctx context.Context, allocationTx string, a.Expiration = sa.Expiration a.OwnerID = sa.OwnerID a.OwnerPublicKey = sa.OwnerPublicKey + a.RepairerID = t.ClientID // blobber node id a.TotalSize = sa.Size a.UsedSize = sa.UsedSize a.Finalized = sa.Finalized - a.PayerID = t.ClientID a.TimeUnit = sa.TimeUnit + a.IsImmutable = sa.IsImmutable // related terms a.Terms = make([]*Terms, 0, len(sa.BlobberDetails)) diff --git a/code/go/0chain.net/blobbercore/blobbergrpc/blobber.pb.go b/code/go/0chain.net/blobbercore/blobbergrpc/blobber.pb.go index 7f2063828..2fb934d7c 100644 --- a/code/go/0chain.net/blobbercore/blobbergrpc/blobber.pb.go +++ b/code/go/0chain.net/blobbercore/blobbergrpc/blobber.pb.go @@ -1435,17 +1435,18 @@ type Allocation struct { UsedSize int64 `protobuf:"varint,4,opt,name=UsedSize,proto3" json:"UsedSize,omitempty"` OwnerID string `protobuf:"bytes,5,opt,name=OwnerID,proto3" json:"OwnerID,omitempty"` OwnerPublicKey string `protobuf:"bytes,6,opt,name=OwnerPublicKey,proto3" json:"OwnerPublicKey,omitempty"` - Expiration int64 `protobuf:"varint,7,opt,name=Expiration,proto3" json:"Expiration,omitempty"` - AllocationRoot string `protobuf:"bytes,8,opt,name=AllocationRoot,proto3" json:"AllocationRoot,omitempty"` - BlobberSize int64 `protobuf:"varint,9,opt,name=BlobberSize,proto3" json:"BlobberSize,omitempty"` - BlobberSizeUsed int64 `protobuf:"varint,10,opt,name=BlobberSizeUsed,proto3" json:"BlobberSizeUsed,omitempty"` - LatestRedeemedWM string `protobuf:"bytes,11,opt,name=LatestRedeemedWM,proto3" json:"LatestRedeemedWM,omitempty"` - IsRedeemRequired bool `protobuf:"varint,12,opt,name=IsRedeemRequired,proto3" json:"IsRedeemRequired,omitempty"` - TimeUnit int64 `protobuf:"varint,13,opt,name=TimeUnit,proto3" json:"TimeUnit,omitempty"` - CleanedUp bool `protobuf:"varint,14,opt,name=CleanedUp,proto3" json:"CleanedUp,omitempty"` - Finalized bool `protobuf:"varint,15,opt,name=Finalized,proto3" json:"Finalized,omitempty"` - Terms []*Term `protobuf:"bytes,16,rep,name=Terms,proto3" json:"Terms,omitempty"` - PayerID string `protobuf:"bytes,17,opt,name=PayerID,proto3" json:"PayerID,omitempty"` + RepairerID string `protobuf:"bytes,7,opt,name=RepairerID,proto3" json:"RepairerID,omitempty"` + PayerID string `protobuf:"bytes,8,opt,name=PayerID,proto3" json:"PayerID,omitempty"` + Expiration int64 `protobuf:"varint,9,opt,name=Expiration,proto3" json:"Expiration,omitempty"` + AllocationRoot string `protobuf:"bytes,10,opt,name=AllocationRoot,proto3" json:"AllocationRoot,omitempty"` + BlobberSize int64 `protobuf:"varint,11,opt,name=BlobberSize,proto3" json:"BlobberSize,omitempty"` + BlobberSizeUsed int64 `protobuf:"varint,12,opt,name=BlobberSizeUsed,proto3" json:"BlobberSizeUsed,omitempty"` + LatestRedeemedWM string `protobuf:"bytes,13,opt,name=LatestRedeemedWM,proto3" json:"LatestRedeemedWM,omitempty"` + IsRedeemRequired bool `protobuf:"varint,14,opt,name=IsRedeemRequired,proto3" json:"IsRedeemRequired,omitempty"` + TimeUnit int64 `protobuf:"varint,15,opt,name=TimeUnit,proto3" json:"TimeUnit,omitempty"` + CleanedUp bool `protobuf:"varint,16,opt,name=CleanedUp,proto3" json:"CleanedUp,omitempty"` + Finalized bool `protobuf:"varint,17,opt,name=Finalized,proto3" json:"Finalized,omitempty"` + Terms []*Term `protobuf:"bytes,18,rep,name=Terms,proto3" json:"Terms,omitempty"` } func (x *Allocation) Reset() { @@ -1522,6 +1523,20 @@ func (x *Allocation) GetOwnerPublicKey() string { return "" } +func (x *Allocation) GetRepairerID() string { + if x != nil { + return x.RepairerID + } + return "" +} + +func (x *Allocation) GetPayerID() string { + if x != nil { + return x.PayerID + } + return "" +} + func (x *Allocation) GetExpiration() int64 { if x != nil { return x.Expiration @@ -1592,13 +1607,6 @@ func (x *Allocation) GetTerms() []*Term { return nil } -func (x *Allocation) GetPayerID() string { - if x != nil { - return x.PayerID - } - return "" -} - type Term struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2319,7 +2327,7 @@ var file_blobber_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x62, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0xb6, 0x04, 0x0a, 0x0a, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x6e, 0x22, 0xd6, 0x04, 0x0a, 0x0a, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x54, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, @@ -2330,31 +2338,33 @@ var file_blobber_proto_rawDesc = []byte{ 0x65, 0x72, 0x49, 0x44, 0x12, 0x26, 0x0a, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, - 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x53, - 0x69, 0x7a, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x42, 0x6c, 0x6f, 0x62, 0x62, - 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, - 0x72, 0x53, 0x69, 0x7a, 0x65, 0x55, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x55, 0x73, 0x65, 0x64, - 0x12, 0x2a, 0x0a, 0x10, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, - 0x65, 0x64, 0x57, 0x4d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x4c, 0x61, 0x74, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x65, 0x64, 0x57, 0x4d, 0x12, 0x2a, 0x0a, 0x10, - 0x49, 0x73, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x49, 0x73, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, - 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x69, 0x6d, 0x65, - 0x55, 0x6e, 0x69, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x54, 0x69, 0x6d, 0x65, - 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x55, - 0x70, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, - 0x55, 0x70, 0x12, 0x1c, 0x0a, 0x09, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, - 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, - 0x12, 0x2e, 0x0a, 0x05, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x18, 0x2e, 0x62, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x52, 0x05, 0x54, 0x65, 0x72, 0x6d, 0x73, - 0x12, 0x18, 0x0a, 0x07, 0x50, 0x61, 0x79, 0x65, 0x72, 0x49, 0x44, 0x18, 0x11, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x50, 0x61, 0x79, 0x65, 0x72, 0x49, 0x44, 0x22, 0x96, 0x01, 0x0a, 0x04, 0x54, + 0x52, 0x65, 0x70, 0x61, 0x69, 0x72, 0x65, 0x72, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x52, 0x65, 0x70, 0x61, 0x69, 0x72, 0x65, 0x72, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, + 0x50, 0x61, 0x79, 0x65, 0x72, 0x49, 0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x50, + 0x61, 0x79, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x20, + 0x0a, 0x0b, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, + 0x12, 0x28, 0x0a, 0x0f, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x55, + 0x73, 0x65, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x42, 0x6c, 0x6f, 0x62, 0x62, + 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x55, 0x73, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x4c, 0x61, + 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x65, 0x64, 0x57, 0x4d, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x64, 0x65, + 0x65, 0x6d, 0x65, 0x64, 0x57, 0x4d, 0x12, 0x2a, 0x0a, 0x10, 0x49, 0x73, 0x52, 0x65, 0x64, 0x65, + 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x10, 0x49, 0x73, 0x52, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, + 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x69, 0x6d, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x18, 0x0f, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x54, 0x69, 0x6d, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x12, 0x1c, 0x0a, 0x09, + 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x05, 0x54, 0x65, + 0x72, 0x6d, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x62, 0x6c, 0x6f, 0x62, + 0x62, 0x65, 0x72, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x65, 0x72, 0x6d, 0x52, 0x05, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x04, 0x54, 0x65, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x62, 0x65, 0x72, 0x49, diff --git a/code/go/0chain.net/blobbercore/blobbergrpc/proto/blobber.proto b/code/go/0chain.net/blobbercore/blobbergrpc/proto/blobber.proto index 449b3d8c3..0fb9e7e3a 100644 --- a/code/go/0chain.net/blobbercore/blobbergrpc/proto/blobber.proto +++ b/code/go/0chain.net/blobbercore/blobbergrpc/proto/blobber.proto @@ -184,17 +184,18 @@ message Allocation { int64 UsedSize = 4; string OwnerID = 5; string OwnerPublicKey = 6; - int64 Expiration = 7; - string AllocationRoot = 8; - int64 BlobberSize = 9; - int64 BlobberSizeUsed = 10; - string LatestRedeemedWM = 11; - bool IsRedeemRequired = 12; - int64 TimeUnit = 13; - bool CleanedUp = 14; - bool Finalized = 15; - repeated Term Terms = 16; - string PayerID = 17; + string RepairerID = 7; + string PayerID = 8; + int64 Expiration = 9; + string AllocationRoot = 10; + int64 BlobberSize = 11; + int64 BlobberSizeUsed = 12; + string LatestRedeemedWM = 13; + bool IsRedeemRequired = 14; + int64 TimeUnit = 15; + bool CleanedUp = 16; + bool Finalized = 17; + repeated Term Terms = 18; } message Term { diff --git a/code/go/0chain.net/blobbercore/challenge/entity.go b/code/go/0chain.net/blobbercore/challenge/entity.go index 283c90f46..c47fc7fc3 100644 --- a/code/go/0chain.net/blobbercore/challenge/entity.go +++ b/code/go/0chain.net/blobbercore/challenge/entity.go @@ -75,6 +75,7 @@ type ChallengeEntity struct { ValidationTickets []*ValidationTicket `json:"validation_tickets" gorm:"-"` ObjectPathString datatypes.JSON `json:"-" gorm:"column:object_path"` ObjectPath *reference.ObjectPath `json:"object_path" gorm:"-"` + Created common.Timestamp `json:"created" gorm:"-"` } func (ChallengeEntity) TableName() string { diff --git a/code/go/0chain.net/blobbercore/challenge/worker.go b/code/go/0chain.net/blobbercore/challenge/worker.go index 7fca5bc7e..1c394d97c 100644 --- a/code/go/0chain.net/blobbercore/challenge/worker.go +++ b/code/go/0chain.net/blobbercore/challenge/worker.go @@ -48,7 +48,6 @@ func SubmitProcessedChallenges(ctx context.Context) error { case <-ctx.Done(): return ctx.Err() default: - Logger.Info("Attempting to commit processed challenges...") rctx := datastore.GetStore().CreateTransaction(ctx) db := datastore.GetStore().GetTransaction(rctx) //lastChallengeRedeemed := &ChallengeEntity{} @@ -199,18 +198,22 @@ func FindChallenges(ctx context.Context) { params := make(map[string]string) params["blobber"] = node.Self.ID + var blobberChallenges BCChallengeResponse blobberChallenges.Challenges = make([]*ChallengeEntity, 0) retBytes, err := transaction.MakeSCRestAPICall(transaction.STORAGE_CONTRACT_ADDRESS, "/openchallenges", params, chain.GetServerChain(), nil) + if err != nil { Logger.Error("Error getting the open challenges from the blockchain", zap.Error(err)) } else { tCtx := datastore.GetStore().CreateTransaction(ctx) db := datastore.GetStore().GetTransaction(tCtx) bytesReader := bytes.NewBuffer(retBytes) + d := json.NewDecoder(bytesReader) d.UseNumber() errd := d.Decode(&blobberChallenges) + if errd != nil { Logger.Error("Error in unmarshal of the sharder response", zap.Error(errd)) } else { @@ -219,8 +222,10 @@ func FindChallenges(ctx context.Context) { Logger.Info("No challenge entity from the challenge map") continue } + challengeObj := v _, err := GetChallengeEntity(tCtx, challengeObj.ChallengeID) + if errors.Is(err, gorm.ErrRecordNotFound) { latestChallenge, err := GetLastChallengeEntity(tCtx) if err == nil || errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/code/go/0chain.net/blobbercore/config/config.go b/code/go/0chain.net/blobbercore/config/config.go index bf95a8d8f..2830bda9d 100644 --- a/code/go/0chain.net/blobbercore/config/config.go +++ b/code/go/0chain.net/blobbercore/config/config.go @@ -63,6 +63,11 @@ const ( DeploymentMainNet = 2 ) +type GeolocationConfig struct { + Latitude float64 `mapstructure:"latitude"` + Longitude float64 `mapstructure:"longitude"` +} + type Config struct { *config.Config DBHost string @@ -118,6 +123,8 @@ type Config struct { NumDelegates int `json:"num_delegates"` // ServiceCharge for blobber. ServiceCharge float64 `json:"service_charge"` + + Geolocation GeolocationConfig `mapstructure:"geolocation"` } /*Configuration of the system */ @@ -133,6 +140,17 @@ func Development() bool { return Configuration.DeploymentMode == DeploymentDevelopment } +// get validated geolocatiion +func Geolocation() GeolocationConfig { + g := Configuration.Geolocation + if g.Latitude > 90.00 || g.Latitude < -90.00 || + g.Longitude > 180.00 || g.Longitude < -180.00 { + panic("Fatal error in config file") + + } + return g +} + /*ErrSupportedChain error for indicating which chain is supported by the server */ var ErrSupportedChain error diff --git a/code/go/0chain.net/blobbercore/filestore/chunk_writer.go b/code/go/0chain.net/blobbercore/filestore/chunk_writer.go new file mode 100644 index 000000000..e1e027f03 --- /dev/null +++ b/code/go/0chain.net/blobbercore/filestore/chunk_writer.go @@ -0,0 +1,115 @@ +package filestore + +import ( + "context" + "errors" + "io" + "os" +) + +//ChunkWriter implements a chunk write that will append content to the file +type ChunkWriter struct { + file string + writer *os.File + reader *os.File + offset int64 + size int64 +} + +//NewChunkWriter create a ChunkWriter +func NewChunkWriter(file string) (*ChunkWriter, error) { + w := &ChunkWriter{ + file: file, + } + var f *os.File + fi, err := os.Stat(file) + if errors.Is(err, os.ErrNotExist) { + f, err = os.Create(file) + if err != nil { + return nil, err + } + } else { + f, err = os.OpenFile(file, os.O_RDONLY|os.O_CREATE|os.O_WRONLY, os.ModeAppend) + if err != nil { + return nil, err + } + + w.size = fi.Size() + w.offset = fi.Size() + } + + w.writer = f + + return w, nil +} + +//Write implements io.Writer +func (w *ChunkWriter) Write(b []byte) (n int, err error) { + if w == nil || w.writer == nil { + return 0, os.ErrNotExist + } + + written, err := w.writer.Write(b) + + w.size += int64(written) + + return written, err +} + +//Reader implements io.Reader +func (w *ChunkWriter) Read(p []byte) (n int, err error) { + if w == nil || w.reader == nil { + reader, err := os.Open(w.file) + + if err != nil { + return 0, err + } + + w.reader = reader + } + + return w.reader.Read(p) +} + +//WriteChunk append data to the file +func (w *ChunkWriter) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { + if w == nil || w.writer == nil { + return 0, os.ErrNotExist + } + + _, err := w.writer.Seek(offset, io.SeekStart) + + if err != nil { + return 0, err + } + + n, err := io.Copy(w.writer, src) + + w.offset += n + w.size += n + + return n, err +} + +//Size length in bytes for regular files +func (w *ChunkWriter) Size() int64 { + if w == nil { + return 0 + } + return w.size +} + +//Close closes the underline File +func (w *ChunkWriter) Close() { + if w == nil { + return + } + + if w.writer != nil { + w.writer.Close() + } + + if w.reader != nil { + w.reader.Close() + } +} diff --git a/code/go/0chain.net/blobbercore/filestore/chunk_writer_test.go b/code/go/0chain.net/blobbercore/filestore/chunk_writer_test.go new file mode 100644 index 000000000..716dc3189 --- /dev/null +++ b/code/go/0chain.net/blobbercore/filestore/chunk_writer_test.go @@ -0,0 +1,93 @@ +package filestore + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWrite(t *testing.T) { + + fileName := filepath.Join(os.TempDir(), "testwrite_"+strconv.FormatInt(time.Now().Unix(), 10)) + + content := "this is full content" + + w, err := NewChunkWriter(fileName) + if err != nil { + require.Error(t, err, "failed to create ChunkWriter") + return + } + + _, err = w.Write([]byte(content)) + + if err != nil { + require.Error(t, err, "failed to ChunkWriter.WriteChunk") + return + } + + buf := make([]byte, w.Size()) + + //read all lines from file + _, err = w.Read(buf) + if err != nil { + require.Error(t, err, "failed to ChunkWriter.Read") + return + } + + assert.Equal(t, content, string(buf), "File content should be same") +} + +func TestWriteChunk(t *testing.T) { + + chunk1 := "this is 1st chunked" + + tempFile, err := ioutil.TempFile("", "") + + if err != nil { + require.Error(t, err, "failed to create tempfile") + return + } + offset, err := tempFile.Write([]byte(chunk1)) + if err != nil { + require.Error(t, err, "failed to write first chunk to tempfile") + return + } + + fileName := tempFile.Name() + tempFile.Close() + + w, err := NewChunkWriter(fileName) + if err != nil { + require.Error(t, err, "failed to create ChunkWriter") + return + } + defer w.Close() + + chunk2 := "this is 2nd chunk" + + _, err = w.WriteChunk(context.TODO(), int64(offset), strings.NewReader(chunk2)) + + if err != nil { + require.Error(t, err, "failed to ChunkWriter.WriteChunk") + return + } + + buf := make([]byte, w.Size()) + + //read all lines from file + _, err = w.Read(buf) + if err != nil { + require.Error(t, err, "failed to ChunkWriter.Read") + return + } + + assert.Equal(t, chunk1+chunk2, string(buf), "File content should be same") +} diff --git a/code/go/0chain.net/blobbercore/filestore/fs_store.go b/code/go/0chain.net/blobbercore/filestore/fs_store.go index 39c99426c..1ab9be0de 100644 --- a/code/go/0chain.net/blobbercore/filestore/fs_store.go +++ b/code/go/0chain.net/blobbercore/filestore/fs_store.go @@ -2,12 +2,14 @@ package filestore import ( "bytes" + "context" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "hash" "io" + "io/ioutil" "mime/multipart" "os" "path/filepath" @@ -503,21 +505,43 @@ func (fs *FileFSStore) WriteFile(allocationID string, fileData *FileInputData, return nil, common.NewError("filestore_setup_error", "Error setting the fs store. "+err.Error()) } - h := sha1.New() tempFilePath := fs.generateTempPath(allocation, fileData, connectionID) - dest, err := os.Create(tempFilePath) + dest, err := NewChunkWriter(tempFilePath) if err != nil { return nil, common.NewError("file_creation_error", err.Error()) } defer dest.Close() - // infile, err := hdr.Open() - // if err != nil { - // return nil, common.NewError("file_reading_error", err.Error()) - // } + + fileRef := &FileOutputData{} + var fileReader io.Reader = infile + + if fileData.IsResumable { + h := sha1.New() + offset, err := dest.WriteChunk(context.TODO(), fileData.UploadOffset, io.TeeReader(fileReader, h)) + + if err != nil { + return nil, common.NewError("file_write_error", err.Error()) + } + + fileRef.ContentHash = hex.EncodeToString(h.Sum(nil)) + fileRef.Size = dest.Size() + fileRef.Name = fileData.Name + fileRef.Path = fileData.Path + fileRef.UploadOffset = fileData.UploadOffset + offset + fileRef.UploadLength = fileData.UploadLength + + if !fileData.IsFinal { + //skip to compute hash until the last chunk is uploaded + return fileRef, nil + } + + fileReader = dest + } + + h := sha1.New() bytesBuffer := bytes.NewBuffer(nil) - //merkleHash := sha3.New256() multiHashWriter := io.MultiWriter(h, bytesBuffer) - tReader := io.TeeReader(infile, multiHashWriter) + tReader := io.TeeReader(fileReader, multiHashWriter) merkleHashes := make([]hash.Hash, 1024) merkleLeaves := make([]util.Hashable, 1024) for idx := range merkleHashes { @@ -525,7 +549,15 @@ func (fs *FileFSStore) WriteFile(allocationID string, fileData *FileInputData, } fileSize := int64(0) for { - written, err := io.CopyN(dest, tReader, CHUNK_SIZE) + var written int64 + + if fileData.IsResumable { + //all chunks have been written, only read bytes from local file , and compute hash + written, err = io.CopyN(ioutil.Discard, tReader, CHUNK_SIZE) + } else { + written, err = io.CopyN(dest, tReader, CHUNK_SIZE) + } + if err != io.EOF && err != nil { return nil, common.NewError("file_write_error", err.Error()) } @@ -541,7 +573,6 @@ func (fs *FileFSStore) WriteFile(allocationID string, fileData *FileInputData, merkleHashes[offset].Write(dataBytes[i:end]) } - // merkleLeaves = append(merkleLeaves, util.NewStringHashable(hex.EncodeToString(merkleHash.Sum(nil)))) bytesBuffer.Reset() if err != nil && err == io.EOF { break @@ -550,17 +581,21 @@ func (fs *FileFSStore) WriteFile(allocationID string, fileData *FileInputData, for idx := range merkleHashes { merkleLeaves[idx] = util.NewStringHashable(hex.EncodeToString(merkleHashes[idx].Sum(nil))) } - //Logger.Info("File size", zap.Int64("file_size", fileSize)) + var mt util.MerkleTreeI = &util.MerkleTree{} mt.ComputeTree(merkleLeaves) - //Logger.Info("Calculated Merkle root", zap.String("merkle_root", mt.GetRoot()), zap.Int("merkle_leaf_count", len(merkleLeaves))) - fileRef := &FileOutputData{} - fileRef.ContentHash = hex.EncodeToString(h.Sum(nil)) + //only update hash for whole file when it is not a resumable upload or is final chunk. + if !fileData.IsResumable || fileData.IsFinal { + fileRef.ContentHash = hex.EncodeToString(h.Sum(nil)) + } + fileRef.Size = fileSize fileRef.Name = fileData.Name fileRef.Path = fileData.Path fileRef.MerkleRoot = mt.GetRoot() + fileRef.UploadOffset = fileSize + fileRef.UploadLength = fileData.UploadLength return fileRef, nil } diff --git a/code/go/0chain.net/blobbercore/filestore/store.go b/code/go/0chain.net/blobbercore/filestore/store.go index 3e2b718fa..6980b4346 100644 --- a/code/go/0chain.net/blobbercore/filestore/store.go +++ b/code/go/0chain.net/blobbercore/filestore/store.go @@ -14,6 +14,15 @@ type FileInputData struct { Path string Hash string OnCloud bool + + //IsResumable the request is resumable upload + IsResumable bool + //UploadLength indicates the size of the entire upload in bytes. The value MUST be a non-negative integer. + UploadLength int64 + //Upload-Offset indicates a byte offset within a resource. The value MUST be a non-negative integer. + UploadOffset int64 + //IsFinal the request is final chunk + IsFinal bool } type FileOutputData struct { @@ -22,6 +31,11 @@ type FileOutputData struct { MerkleRoot string ContentHash string Size int64 + + //UploadLength indicates the size of the entire upload in bytes. The value MUST be a non-negative integer. + UploadLength int64 + //Upload-Offset indicates a byte offset within a resource. The value MUST be a non-negative integer. + UploadOffset int64 } type FileObjectHandler func(contentHash string, contentSize int64) diff --git a/code/go/0chain.net/blobbercore/handler/convert.go b/code/go/0chain.net/blobbercore/handler/convert.go index bc5f699f5..a61b61b31 100644 --- a/code/go/0chain.net/blobbercore/handler/convert.go +++ b/code/go/0chain.net/blobbercore/handler/convert.go @@ -25,6 +25,8 @@ func AllocationToGRPCAllocation(alloc *allocation.Allocation) *blobbergrpc.Alloc UsedSize: alloc.UsedSize, OwnerID: alloc.OwnerID, OwnerPublicKey: alloc.OwnerPublicKey, + RepairerID: alloc.RepairerID, + PayerID: alloc.PayerID, Expiration: int64(alloc.Expiration), AllocationRoot: alloc.AllocationRoot, BlobberSize: alloc.BlobberSize, @@ -35,7 +37,6 @@ func AllocationToGRPCAllocation(alloc *allocation.Allocation) *blobbergrpc.Alloc CleanedUp: alloc.CleanedUp, Finalized: alloc.Finalized, Terms: terms, - PayerID: alloc.PayerID, } } diff --git a/code/go/0chain.net/blobbercore/handler/dto.go b/code/go/0chain.net/blobbercore/handler/dto.go index 2edcf2b26..98bc27aee 100644 --- a/code/go/0chain.net/blobbercore/handler/dto.go +++ b/code/go/0chain.net/blobbercore/handler/dto.go @@ -12,6 +12,11 @@ type UploadResult struct { Size int64 `json:"size"` Hash string `json:"content_hash"` MerkleRoot string `json:"merkle_root"` + + //UploadLength indicates the size of the entire upload in bytes. The value MUST be a non-negative integer. + UploadLength int64 `json:"upload_length"` + //Upload-Offset indicates a byte offset within a resource. The value MUST be a non-negative integer. + UploadOffset int64 `json:"upload_offset"` } type CommitResult struct { diff --git a/code/go/0chain.net/blobbercore/handler/grpc_handler.go b/code/go/0chain.net/blobbercore/handler/grpc_handler.go index dfb361511..8a1f23ff4 100644 --- a/code/go/0chain.net/blobbercore/handler/grpc_handler.go +++ b/code/go/0chain.net/blobbercore/handler/grpc_handler.go @@ -44,11 +44,10 @@ func (b *blobberGRPCService) GetAllocation(ctx context.Context, request *blobber func (b *blobberGRPCService) GetFileMetaData(ctx context.Context, req *blobbergrpc.GetFileMetaDataRequest) (*blobbergrpc.GetFileMetaDataResponse, error) { logger := ctxzap.Extract(ctx) - allocationObj, err := b.storageHandler.verifyAllocation(ctx, req.Allocation, true) + alloc, err := b.storageHandler.verifyAllocation(ctx, req.Allocation, true) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID clientID := req.Context.Client if len(clientID) == 0 { @@ -61,10 +60,10 @@ func (b *blobberGRPCService) GetFileMetaData(ctx context.Context, req *blobbergr if len(path) == 0 { return nil, common.NewError("invalid_parameters", "Invalid path") } - path_hash = reference.GetReferenceLookup(allocationID, path) + path_hash = reference.GetReferenceLookup(alloc.ID, path) } - fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, allocationID, path_hash) + fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, alloc.ID, path_hash) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } @@ -84,18 +83,22 @@ func (b *blobberGRPCService) GetFileMetaData(ctx context.Context, req *blobbergr logger.Error("Failed to get collaborators from refID", zap.Error(err), zap.Any("ref_id", fileref.ID)) } - authTokenString := req.AuthToken - - if (allocationObj.OwnerID != clientID && - allocationObj.PayerID != clientID && - !b.packageHandler.IsACollaborator(ctx, fileref.ID, clientID)) || len(authTokenString) > 0 { - authTicketVerified, err := b.storageHandler.verifyAuthTicket(ctx, req.AuthToken, allocationObj, fileref, clientID) - if err != nil { - return nil, err - } - if !authTicketVerified { - return nil, common.NewError("auth_ticket_verification_failed", "Could not verify the auth ticket.") + // authorize file access + var ( + isOwner = clientID == alloc.OwnerID + isRepairer = clientID == alloc.RepairerID + isCollaborator = b.packageHandler.IsACollaborator(ctx, fileref.ID, clientID) + ) + + if !isOwner && !isRepairer && !isCollaborator { + // check auth token + if isAuthorized, err := b.storageHandler.verifyAuthTicket(ctx, + req.AuthToken, alloc, fileref, clientID, + ); !isAuthorized { + return nil, common.NewErrorf("download_file", + "cannot verify auth ticket: %v", err) } + fileref.Path = "" } @@ -116,15 +119,14 @@ func (b *blobberGRPCService) GetFileMetaData(ctx context.Context, req *blobbergr func (b *blobberGRPCService) GetFileStats(ctx context.Context, req *blobbergrpc.GetFileStatsRequest) (*blobbergrpc.GetFileStatsResponse, error) { allocationTx := req.Context.Allocation - allocationObj, err := b.storageHandler.verifyAllocation(ctx, allocationTx, true) + alloc, err := b.storageHandler.verifyAllocation(ctx, allocationTx, true) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID clientID := req.Context.Client - if len(clientID) == 0 || allocationObj.OwnerID != clientID { + if len(clientID) == 0 || alloc.OwnerID != clientID { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") } @@ -134,10 +136,10 @@ func (b *blobberGRPCService) GetFileStats(ctx context.Context, req *blobbergrpc. if len(path) == 0 { return nil, common.NewError("invalid_parameters", "Invalid path") } - path_hash = reference.GetReferenceLookup(allocationID, path) + path_hash = reference.GetReferenceLookup(alloc.ID, path) } - fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, allocationID, path_hash) + fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, alloc.ID, path_hash) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) @@ -164,12 +166,11 @@ func (b *blobberGRPCService) ListEntities(ctx context.Context, req *blobbergrpc. clientID := req.Context.Client allocationTx := req.Context.Allocation - allocationObj, err := b.storageHandler.verifyAllocation(ctx, allocationTx, true) + alloc, err := b.storageHandler.verifyAllocation(ctx, allocationTx, true) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID if len(clientID) == 0 { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") @@ -181,18 +182,18 @@ func (b *blobberGRPCService) ListEntities(ctx context.Context, req *blobbergrpc. if len(path) == 0 { return nil, common.NewError("invalid_parameters", "Invalid path") } - path_hash = reference.GetReferenceLookup(allocationID, path) + path_hash = reference.GetReferenceLookup(alloc.ID, path) } logger.Info("Path Hash for list dir :" + path_hash) - fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, allocationID, path_hash) + fileref, err := b.packageHandler.GetReferenceFromLookupHash(ctx, alloc.ID, path_hash) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid path. "+err.Error()) } authTokenString := req.AuthToken - if clientID != allocationObj.OwnerID || len(authTokenString) > 0 { - authTicketVerified, err := b.storageHandler.verifyAuthTicket(ctx, authTokenString, allocationObj, fileref, clientID) + if clientID != alloc.OwnerID || len(authTokenString) > 0 { + authTicketVerified, err := b.storageHandler.verifyAuthTicket(ctx, authTokenString, alloc, fileref, clientID) if err != nil { return nil, err } @@ -201,18 +202,18 @@ func (b *blobberGRPCService) ListEntities(ctx context.Context, req *blobbergrpc. } } - dirref, err := b.packageHandler.GetRefWithChildren(ctx, allocationID, fileref.Path) + dirref, err := b.packageHandler.GetRefWithChildren(ctx, alloc.ID, fileref.Path) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid path. "+err.Error()) } - if clientID != allocationObj.OwnerID { + if clientID != alloc.OwnerID { dirref.Path = "" } var entities []*blobbergrpc.FileRef for _, entity := range dirref.Children { - if clientID != allocationObj.OwnerID { + if clientID != alloc.OwnerID { entity.Path = "" } entities = append(entities, reference.FileRefToFileRefGRPC(entity)) @@ -221,22 +222,21 @@ func (b *blobberGRPCService) ListEntities(ctx context.Context, req *blobbergrpc. refGRPC.DirMetaData.Children = entities return &blobbergrpc.ListEntitiesResponse{ - AllocationRoot: allocationObj.AllocationRoot, + AllocationRoot: alloc.AllocationRoot, MetaData: refGRPC, }, nil } func (b *blobberGRPCService) GetObjectPath(ctx context.Context, req *blobbergrpc.GetObjectPathRequest) (*blobbergrpc.GetObjectPathResponse, error) { allocationTx := req.Context.Allocation - allocationObj, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) + alloc, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID clientID := req.Context.Client - if len(clientID) == 0 || allocationObj.OwnerID != clientID { + if len(clientID) == 0 || alloc.OwnerID != clientID { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") } path := req.Path @@ -254,16 +254,16 @@ func (b *blobberGRPCService) GetObjectPath(ctx context.Context, req *blobbergrpc return nil, common.NewError("invalid_parameters", "Invalid block number") } - objectPath, err := b.packageHandler.GetObjectPathGRPC(ctx, allocationID, blockNum) + objectPath, err := b.packageHandler.GetObjectPathGRPC(ctx, alloc.ID, blockNum) if err != nil { return nil, err } var latestWM *writemarker.WriteMarkerEntity - if len(allocationObj.AllocationRoot) == 0 { + if len(alloc.AllocationRoot) == 0 { latestWM = nil } else { - latestWM, err = b.packageHandler.GetWriteMarkerEntity(ctx, allocationObj.AllocationRoot) + latestWM, err = b.packageHandler.GetWriteMarkerEntity(ctx, alloc.AllocationRoot) if err != nil { return nil, common.NewError("latest_write_marker_read_error", "Error reading the latest write marker for allocation."+err.Error()) } @@ -281,12 +281,11 @@ func (b *blobberGRPCService) GetObjectPath(ctx context.Context, req *blobbergrpc func (b *blobberGRPCService) GetReferencePath(ctx context.Context, req *blobbergrpc.GetReferencePathRequest) (*blobbergrpc.GetReferencePathResponse, error) { allocationTx := req.Context.Allocation - allocationObj, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) + alloc, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID clientID := req.Context.Client if len(clientID) == 0 { @@ -308,7 +307,7 @@ func (b *blobberGRPCService) GetReferencePath(ctx context.Context, req *blobberg } } - rootRef, err := b.packageHandler.GetReferencePathFromPaths(ctx, allocationID, paths) + rootRef, err := b.packageHandler.GetReferencePathFromPaths(ctx, alloc.ID, paths) if err != nil { return nil, err } @@ -330,10 +329,10 @@ func (b *blobberGRPCService) GetReferencePath(ctx context.Context, req *blobberg } var latestWM *writemarker.WriteMarkerEntity - if len(allocationObj.AllocationRoot) == 0 { + if len(alloc.AllocationRoot) == 0 { latestWM = nil } else { - latestWM, err = writemarker.GetWriteMarkerEntity(ctx, allocationObj.AllocationRoot) + latestWM, err = writemarker.GetWriteMarkerEntity(ctx, alloc.AllocationRoot) if err != nil { return nil, common.NewError("latest_write_marker_read_error", "Error reading the latest write marker for allocation."+err.Error()) } @@ -349,15 +348,14 @@ func (b *blobberGRPCService) GetReferencePath(ctx context.Context, req *blobberg func (b *blobberGRPCService) GetObjectTree(ctx context.Context, req *blobbergrpc.GetObjectTreeRequest) (*blobbergrpc.GetObjectTreeResponse, error) { allocationTx := req.Context.Allocation - allocationObj, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) + alloc, err := b.storageHandler.verifyAllocation(ctx, allocationTx, false) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID clientID := req.Context.Client - if len(clientID) == 0 || allocationObj.OwnerID != clientID { + if len(clientID) == 0 || alloc.OwnerID != clientID { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") } path := req.Path @@ -365,7 +363,7 @@ func (b *blobberGRPCService) GetObjectTree(ctx context.Context, req *blobbergrpc return nil, common.NewError("invalid_parameters", "Invalid path") } - rootRef, err := b.packageHandler.GetObjectTree(ctx, allocationID, path) + rootRef, err := b.packageHandler.GetObjectTree(ctx, alloc.ID, path) if err != nil { return nil, err } @@ -387,10 +385,10 @@ func (b *blobberGRPCService) GetObjectTree(ctx context.Context, req *blobbergrpc } var latestWM *writemarker.WriteMarkerEntity - if len(allocationObj.AllocationRoot) == 0 { + if len(alloc.AllocationRoot) == 0 { latestWM = nil } else { - latestWM, err = writemarker.GetWriteMarkerEntity(ctx, allocationObj.AllocationRoot) + latestWM, err = writemarker.GetWriteMarkerEntity(ctx, alloc.AllocationRoot) if err != nil { return nil, common.NewError("latest_write_marker_read_error", "Error reading the latest write marker for allocation."+err.Error()) } diff --git a/code/go/0chain.net/blobbercore/handler/helper.go b/code/go/0chain.net/blobbercore/handler/helper.go index b412aa718..4cced3dbb 100644 --- a/code/go/0chain.net/blobbercore/handler/helper.go +++ b/code/go/0chain.net/blobbercore/handler/helper.go @@ -16,12 +16,9 @@ import ( ) func setupGRPCHandlerContext(ctx context.Context, r *blobbergrpc.RequestContext) context.Context { - ctx = context.WithValue(ctx, constants.CLIENT_CONTEXT_KEY, - r.Client) - ctx = context.WithValue(ctx, constants.CLIENT_KEY_CONTEXT_KEY, - r.ClientKey) - ctx = context.WithValue(ctx, constants.ALLOCATION_CONTEXT_KEY, - r.Allocation) + ctx = context.WithValue(ctx, constants.CLIENT_CONTEXT_KEY, r.Client) + ctx = context.WithValue(ctx, constants.CLIENT_KEY_CONTEXT_KEY, r.ClientKey) + ctx = context.WithValue(ctx, constants.ALLOCATION_CONTEXT_KEY, r.Allocation) return ctx } diff --git a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go index d7fb4eba1..5f43e3946 100644 --- a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go +++ b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go @@ -184,38 +184,34 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( "invalid method used (GET), use POST instead") } + // get client and allocation ids var ( - allocationTx = ctx.Value(constants.ALLOCATION_CONTEXT_KEY).(string) clientID = ctx.Value(constants.CLIENT_CONTEXT_KEY).(string) - - allocationObj *allocation.Allocation + allocationTx = ctx.Value(constants.ALLOCATION_CONTEXT_KEY).(string) + _ = ctx.Value(constants.CLIENT_KEY_CONTEXT_KEY).(string) // runtime type check + alloc *allocation.Allocation ) + // check client if len(clientID) == 0 { return nil, common.NewError("download_file", "invalid client") } - // runtime type check - _ = ctx.Value(constants.CLIENT_KEY_CONTEXT_KEY).(string) - - // verify or update allocation - allocationObj, err = fsh.verifyAllocation(ctx, allocationTx, false) + // get and check allocation + alloc, err = fsh.verifyAllocation(ctx, allocationTx, false) if err != nil { return nil, common.NewErrorf("download_file", "invalid allocation id passed: %v", err) } - var allocationID = allocationObj.ID - + // get and parse file params if err = r.ParseMultipartForm(FORM_FILE_PARSE_MAX_MEMORY); nil != err { Logger.Info("download_file - request_parse_error", zap.Error(err)) return nil, common.NewErrorf("download_file", "request_parse_error: %v", err) } - rxPay := r.FormValue("rx_pay") == "true" - - pathHash, err := pathHashFromReq(r, allocationID) + pathHash, err := pathHashFromReq(r, alloc.ID) if err != nil { return nil, common.NewError("download_file", "invalid path") } @@ -243,6 +239,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( "invalid number of blocks") } + // get read marker var ( readMarkerString = r.FormValue("read_marker") readMarker = &readmarker.ReadMarker{} @@ -256,14 +253,14 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( var rmObj = &readmarker.ReadMarkerEntity{} rmObj.LatestRM = readMarker - if err = rmObj.VerifyMarker(ctx, allocationObj); err != nil { + if err = rmObj.VerifyMarker(ctx, alloc); err != nil { return nil, common.NewErrorf("download_file", "invalid read marker, "+ "failed to verify the read marker: %v", err) } + // get file reference var fileref *reference.Ref - fileref, err = reference.GetReferenceFromLookupHash(ctx, allocationID, - pathHash) + fileref, err = reference.GetReferenceFromLookupHash(ctx, alloc.ID, pathHash) if err != nil { return nil, common.NewErrorf("download_file", "invalid file path: %v", err) @@ -274,40 +271,37 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( "path is not a file: %v", err) } - var ( - authTokenString = r.FormValue("auth_token") - clientIDForReadRedeem = clientID // default payer is client - isACollaborator = reference.IsACollaborator(ctx, fileref.ID, clientID) - ) + // set payer: default + var payerID = alloc.OwnerID - // Owner will pay for collaborator - if isACollaborator { - clientIDForReadRedeem = allocationObj.OwnerID + // set payer: check for explicit allocation payer value + if len(alloc.PayerID) > 0 { + payerID = alloc.PayerID } - var attrs *reference.Attributes - if attrs, err = fileref.GetAttributes(); err != nil { - return nil, common.NewErrorf("download_file", - "error getting file attributes: %v", err) + // set payer: check for command line payer flag (--rx_pay) + if r.FormValue("rx_pay") == "true" { + payerID = clientID } - var authToken *readmarker.AuthTicket = nil + // authorize file access + var ( + isOwner = clientID == alloc.OwnerID + isRepairer = clientID == alloc.RepairerID + isCollaborator = reference.IsACollaborator(ctx, fileref.ID, clientID) + ) - if (allocationObj.OwnerID != clientID && - allocationObj.PayerID != clientID && - !isACollaborator) || len(authTokenString) > 0 { + var authToken *readmarker.AuthTicket = nil - var authTicketVerified bool - authTicketVerified, err = fsh.verifyAuthTicket(ctx, r.FormValue("auth_token"), allocationObj, - fileref, clientID) - if err != nil { - return nil, common.NewErrorf("download_file", - "verifying auth ticket: %v", err) - } + if !isOwner && !isRepairer && !isCollaborator { + var authTokenString = r.FormValue("auth_token") - if !authTicketVerified { + // check auth token + if isAuthorized, err := fsh.verifyAuthTicket(ctx, + authTokenString, alloc, fileref, clientID, + ); !isAuthorized { return nil, common.NewErrorf("download_file", - "could not verify the auth ticket") + "cannot verify auth ticket: %v", err) } authToken = &readmarker.AuthTicket{} @@ -335,22 +329,26 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( return nil, errors.New("auth ticket is not authorized to download file specified") } } + readMarker.AuthTicket = datatypes.JSON(authTokenString) - // if --rx_pay used 3rd_party pays - if rxPay { - clientIDForReadRedeem = clientID - } else if attrs.WhoPaysForReads == common.WhoPaysOwner { - clientIDForReadRedeem = allocationObj.OwnerID // owner pays + // check for file payer flag + if fileAttrs, err := fileref.GetAttributes(); err != nil { + return nil, common.NewErrorf("download_file", + "error getting file attributes: %v", err) + } else { + if fileAttrs.WhoPaysForReads == common.WhoPays3rdParty { + payerID = clientID + } } - - readMarker.AuthTicket = datatypes.JSON(authTokenString) } + // create read marker var ( rme *readmarker.ReadMarkerEntity latestRM *readmarker.ReadMarker pendNumBlocks int64 ) + rme, err = readmarker.GetLatestReadMarkerEntity(ctx, clientID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.NewErrorf("download_file", @@ -378,15 +376,13 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( } // check out read pool tokens if read_price > 0 - err = readPreRedeem(ctx, allocationObj, numBlocks, pendNumBlocks, - clientIDForReadRedeem) + err = readPreRedeem(ctx, alloc, numBlocks, pendNumBlocks, payerID) if err != nil { return nil, common.NewErrorf("download_file", "pre-redeeming read marker: %v", err) } - // reading allowed - + // reading is allowed var ( downloadMode = r.FormValue("content") respData []byte @@ -397,7 +393,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( fileData.Path = fileref.Path fileData.Hash = fileref.ThumbnailHash fileData.OnCloud = fileref.OnCloud - respData, err = filestore.GetFileStore().GetFileBlock(allocationID, + respData, err = filestore.GetFileStore().GetFileBlock(alloc.ID, fileData, blockNum, numBlocks) if err != nil { return nil, common.NewErrorf("download_file", @@ -409,7 +405,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( fileData.Path = fileref.Path fileData.Hash = fileref.ContentHash fileData.OnCloud = fileref.OnCloud - respData, err = filestore.GetFileStore().GetFileBlock(allocationID, + respData, err = filestore.GetFileStore().GetFileBlock(alloc.ID, fileData, blockNum, numBlocks) if err != nil { return nil, common.NewErrorf("download_file", @@ -417,7 +413,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( } } - readMarker.PayerID = clientIDForReadRedeem + readMarker.PayerID = payerID err = readmarker.SaveLatestReadMarker(ctx, readMarker, latestRM == nil) if err != nil { return nil, common.NewErrorf("download_file", @@ -511,6 +507,10 @@ func (fsh *StorageHandler) CommitWrite(ctx context.Context, r *http.Request) (*C return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } + if allocationObj.IsImmutable { + return nil, common.NewError("immutable_allocation", "Cannot write to an immutable allocation") + } + allocationID := allocationObj.ID connectionID := r.FormValue("connection_id") @@ -532,7 +532,7 @@ func (fsh *StorageHandler) CommitWrite(ctx context.Context, r *http.Request) (*C "Invalid connection id. Connection does not have any changes.") } - var isACollaborator bool + var isCollaborator bool for _, change := range connectionObj.Changes { if change.Operation == allocation.UPDATE_OPERATION { updateFileChange := new(allocation.UpdateFileChange) @@ -543,7 +543,7 @@ func (fsh *StorageHandler) CommitWrite(ctx context.Context, r *http.Request) (*C if err != nil { return nil, err } - isACollaborator = reference.IsACollaborator(ctx, fileRef.ID, clientID) + isCollaborator = reference.IsACollaborator(ctx, fileRef.ID, clientID) break } } @@ -552,7 +552,7 @@ func (fsh *StorageHandler) CommitWrite(ctx context.Context, r *http.Request) (*C return nil, common.NewError("invalid_params", "Please provide clientID and clientKey") } - if (allocationObj.OwnerID != clientID || encryption.Hash(clientKeyBytes) != clientID) && !isACollaborator { + if (allocationObj.OwnerID != clientID || encryption.Hash(clientKeyBytes) != clientID) && !isCollaborator { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") } @@ -603,7 +603,7 @@ func (fsh *StorageHandler) CommitWrite(ctx context.Context, r *http.Request) (*C } var clientIDForWriteRedeem = writeMarker.ClientID - if isACollaborator { + if isCollaborator { clientIDForWriteRedeem = allocationObj.OwnerID } @@ -677,6 +677,11 @@ func (fsh *StorageHandler) RenameObject(ctx context.Context, r *http.Request) (i if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } + + if allocationObj.IsImmutable { + return nil, common.NewError("immutable_allocation", "Cannot rename data in an immutable allocation") + } + allocationID := allocationObj.ID clientID := ctx.Value(constants.CLIENT_CONTEXT_KEY).(string) @@ -775,6 +780,10 @@ func (fsh *StorageHandler) UpdateObjectAttributes(ctx context.Context, return nil, common.NewError("invalid_signature", "Invalid signature") } + if alloc.IsImmutable { + return nil, common.NewError("immutable_allocation", "Cannot update data in an immutable allocation") + } + // runtime type check _ = ctx.Value(constants.CLIENT_KEY_CONTEXT_KEY).(string) @@ -871,6 +880,10 @@ func (fsh *StorageHandler) CopyObject(ctx context.Context, r *http.Request) (int return nil, common.NewError("invalid_signature", "Invalid signature") } + if allocationObj.IsImmutable { + return nil, common.NewError("immutable_allocation", "Cannot copy data in an immutable allocation") + } + clientID := ctx.Value(constants.CLIENT_CONTEXT_KEY).(string) _ = ctx.Value(constants.CLIENT_KEY_CONTEXT_KEY).(string) @@ -1003,6 +1016,10 @@ func (fsh *StorageHandler) WriteFile(ctx context.Context, r *http.Request) (*Upl return nil, common.NewError("invalid_signature", "Invalid signature") } + if allocationObj.IsImmutable { + return nil, common.NewError("immutable_allocation", "Cannot write to an immutable allocation") + } + allocationID := allocationObj.ID if len(clientID) == 0 { @@ -1037,7 +1054,7 @@ func (fsh *StorageHandler) WriteFile(ctx context.Context, r *http.Request) (*Upl } if mode == allocation.DELETE_OPERATION { - if allocationObj.OwnerID != clientID && allocationObj.PayerID != clientID { + if allocationObj.OwnerID != clientID && allocationObj.RepairerID != clientID { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner or the payer of the allocation") } result, err = fsh.DeleteFile(ctx, r, connectionObj) @@ -1060,7 +1077,7 @@ func (fsh *StorageHandler) WriteFile(ctx context.Context, r *http.Request) (*Upl existingFileRefSize := int64(0) exisitingFileOnCloud := false if mode == allocation.INSERT_OPERATION { - if allocationObj.OwnerID != clientID && allocationObj.PayerID != clientID { + if allocationObj.OwnerID != clientID && allocationObj.RepairerID != clientID { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner or the payer of the allocation") } @@ -1073,7 +1090,7 @@ func (fsh *StorageHandler) WriteFile(ctx context.Context, r *http.Request) (*Upl } if allocationObj.OwnerID != clientID && - allocationObj.PayerID != clientID && + allocationObj.RepairerID != clientID && !reference.IsACollaborator(ctx, exisitingFileRef.ID, clientID) { return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner, collaborator or the payer of the allocation") } diff --git a/code/go/0chain.net/blobbercore/handler/protocol.go b/code/go/0chain.net/blobbercore/handler/protocol.go index 9a4bfa9e8..a490de673 100644 --- a/code/go/0chain.net/blobbercore/handler/protocol.go +++ b/code/go/0chain.net/blobbercore/handler/protocol.go @@ -1,19 +1,20 @@ package handler import ( - "context" - "encoding/json" - "errors" "sync" "time" + "errors" + "context" + "encoding/json" + "go.uber.org/zap" + "github.com/0chain/gosdk/zcncore" "0chain.net/blobbercore/config" - . "0chain.net/core/logging" + "0chain.net/core/chain" "0chain.net/core/node" "0chain.net/core/transaction" - - "github.com/0chain/gosdk/zcncore" - "go.uber.org/zap" + "0chain.net/core/util" + . "0chain.net/core/logging" ) const ( @@ -56,21 +57,50 @@ func (ar *apiResp) err() error { //nolint:unused,deadcode // might be used later return nil } -func RegisterBlobber(ctx context.Context) (string, error) { - wcb := &WalletCallback{} - wcb.wg = &sync.WaitGroup{} - wcb.wg.Add(1) - err := zcncore.RegisterToMiners(node.Self.GetWallet(), wcb) - if err != nil { - return "", err +func getStorageNode() (*transaction.StorageNode, error) { + var err error + sn := &transaction.StorageNode{} + sn.ID = node.Self.ID + sn.BaseURL = node.Self.GetURLBase() + sn.Geolocation = transaction.StorageNodeGeolocation(config.Geolocation()) + sn.Capacity = config.Configuration.Capacity + readPrice := config.Configuration.ReadPrice + writePrice := config.Configuration.WritePrice + if config.Configuration.PriceInUSD { + readPrice, err = zcncore.ConvertUSDToToken(readPrice) + if err != nil { + return nil, err + } + + writePrice, err = zcncore.ConvertUSDToToken(writePrice) + if err != nil { + return nil, err + } } + sn.Terms.ReadPrice = zcncore.ConvertToValue(readPrice) + sn.Terms.WritePrice = zcncore.ConvertToValue(writePrice) + sn.Terms.MinLockDemand = config.Configuration.MinLockDemand + sn.Terms.MaxOfferDuration = config.Configuration.MaxOfferDuration + sn.Terms.ChallengeCompletionTime = config.Configuration.ChallengeCompletionTime + sn.StakePoolSettings.DelegateWallet = config.Configuration.DelegateWallet + sn.StakePoolSettings.MinStake = config.Configuration.MinStake + sn.StakePoolSettings.MaxStake = config.Configuration.MaxStake + sn.StakePoolSettings.NumDelegates = config.Configuration.NumDelegates + sn.StakePoolSettings.ServiceCharge = config.Configuration.ServiceCharge + return sn, nil +} + +// Add or update blobber on blockchain +func BlobberAdd(ctx context.Context) (string, error) { time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) + // initialize storage node (ie blobber) txn, err := transaction.NewTransactionEntity() if err != nil { return "", err } + sn, err := getStorageNode() if err != nil { return "", err @@ -80,11 +110,13 @@ func RegisterBlobber(ctx context.Context) (string, error) { if err != nil { return "", err } - Logger.Info("Adding blobber to the blockchain.") + + Logger.Info("Adding or updating on the blockchain") + err = txn.ExecuteSmartContract(transaction.STORAGE_CONTRACT_ADDRESS, transaction.ADD_BLOBBER_SC_NAME, string(snBytes), 0) if err != nil { - Logger.Info("Failed during registering blobber to the mining network", + Logger.Info("Failed to set blobber on the blockchain", zap.String("err:", err.Error())) return "", err } @@ -102,15 +134,16 @@ func BlobberHealthCheck(ctx context.Context) (string, error) { if config.Configuration.Capacity == 0 { return "", ErrBlobberHasRemoved } + txn, err := transaction.NewTransactionEntity() if err != nil { return "", err } - Logger.Info("Blobber health check to the blockchain.") + err = txn.ExecuteSmartContract(transaction.STORAGE_CONTRACT_ADDRESS, transaction.BLOBBER_HEALTH_CHECK, "", 0) if err != nil { - Logger.Info("Failed during blobber health check to the mining network", + Logger.Info("Failed to health check on the blockchain", zap.String("err:", err.Error())) return "", err } @@ -118,63 +151,26 @@ func BlobberHealthCheck(ctx context.Context) (string, error) { return txn.Hash, nil } -func UpdateBlobberSettings(ctx context.Context) (string, error) { - txn, err := transaction.NewTransactionEntity() - if err != nil { - return "", err - } - - sn, err := getStorageNode() - if err != nil { - return "", err - } - - snBytes, err := json.Marshal(sn) - if err != nil { - return "", err - } +func TransactionVerify(txnHash string) (t *transaction.Transaction, err error) { + time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) - Logger.Info("Updating settings to the blockchain.") - err = txn.ExecuteSmartContract(transaction.STORAGE_CONTRACT_ADDRESS, - transaction.UPDATE_BLOBBER_SETTINGS, string(snBytes), 0) - if err != nil { - Logger.Info("Failed during updating settings to the mining network", - zap.String("err:", err.Error())) - return "", err + for i := 0; i < util.MAX_RETRIES; i++ { + time.Sleep(transaction.SLEEP_FOR_TXN_CONFIRMATION * time.Second) + if t, err = transaction.VerifyTransaction(txnHash, chain.GetServerChain()); err == nil { + return t, nil + } } - return txn.Hash, nil + return } -func getStorageNode() (*transaction.StorageNode, error) { - var err error - sn := &transaction.StorageNode{} - sn.ID = node.Self.ID - sn.BaseURL = node.Self.GetURLBase() - sn.Capacity = config.Configuration.Capacity - readPrice := config.Configuration.ReadPrice - writePrice := config.Configuration.WritePrice - if config.Configuration.PriceInUSD { - readPrice, err = zcncore.ConvertUSDToToken(readPrice) - if err != nil { - return nil, err - } - - writePrice, err = zcncore.ConvertUSDToToken(writePrice) - if err != nil { - return nil, err - } +func WalletRegister() error { + wcb := &WalletCallback{} + wcb.wg = &sync.WaitGroup{} + wcb.wg.Add(1) + if err := zcncore.RegisterToMiners(node.Self.GetWallet(), wcb); err != nil { + return err } - sn.Terms.ReadPrice = zcncore.ConvertToValue(readPrice) - sn.Terms.WritePrice = zcncore.ConvertToValue(writePrice) - sn.Terms.MinLockDemand = config.Configuration.MinLockDemand - sn.Terms.MaxOfferDuration = config.Configuration.MaxOfferDuration - sn.Terms.ChallengeCompletionTime = config.Configuration.ChallengeCompletionTime - sn.StakePoolSettings.DelegateWallet = config.Configuration.DelegateWallet - sn.StakePoolSettings.MinStake = config.Configuration.MinStake - sn.StakePoolSettings.MaxStake = config.Configuration.MaxStake - sn.StakePoolSettings.NumDelegates = config.Configuration.NumDelegates - sn.StakePoolSettings.ServiceCharge = config.Configuration.ServiceCharge - return sn, nil + return nil } diff --git a/code/go/0chain.net/blobbercore/handler/storage_handler.go b/code/go/0chain.net/blobbercore/handler/storage_handler.go index a8fdc366b..a6dbb6daa 100644 --- a/code/go/0chain.net/blobbercore/handler/storage_handler.go +++ b/code/go/0chain.net/blobbercore/handler/storage_handler.go @@ -123,12 +123,12 @@ func (fsh *StorageHandler) GetFileMeta(ctx context.Context, r *http.Request) (in return nil, common.NewError("invalid_method", "Invalid method used. Use POST instead") } allocationTx := ctx.Value(constants.ALLOCATION_CONTEXT_KEY).(string) - allocationObj, err := fsh.verifyAllocation(ctx, allocationTx, true) + alloc, err := fsh.verifyAllocation(ctx, allocationTx, true) if err != nil { return nil, common.NewError("invalid_parameters", "Invalid allocation id passed."+err.Error()) } - allocationID := allocationObj.ID + allocationID := alloc.ID clientID := ctx.Value(constants.CLIENT_CONTEXT_KEY).(string) if len(clientID) == 0 { @@ -164,18 +164,24 @@ func (fsh *StorageHandler) GetFileMeta(ctx context.Context, r *http.Request) (in result["collaborators"] = collaborators - authTokenString := r.FormValue("auth_token") + // authorize file access + var ( + isOwner = clientID == alloc.OwnerID + isRepairer = clientID == alloc.RepairerID + isCollaborator = reference.IsACollaborator(ctx, fileref.ID, clientID) + ) - if (allocationObj.OwnerID != clientID && - allocationObj.PayerID != clientID && - !reference.IsACollaborator(ctx, fileref.ID, clientID)) || len(authTokenString) > 0 { - authTicketVerified, err := fsh.verifyAuthTicket(ctx, r.FormValue("auth_token"), allocationObj, fileref, clientID) - if err != nil { - return nil, err - } - if !authTicketVerified { - return nil, common.NewError("auth_ticket_verification_failed", "Could not verify the auth ticket.") + if !isOwner && !isRepairer && !isCollaborator { + var authTokenString = r.FormValue("auth_token") + + // check auth token + if isAuthorized, err := fsh.verifyAuthTicket(ctx, + authTokenString, alloc, fileref, clientID, + ); !isAuthorized { + return nil, common.NewErrorf("download_file", + "cannot verify auth ticket: %v", err) } + delete(result, "path") } @@ -696,7 +702,7 @@ func (fsh *StorageHandler) CalculateHash(ctx context.Context, r *http.Request) ( // verifySignatureFromRequest verifyes signature passed as common.ClientSignatureHeader header. func verifySignatureFromRequest(r *http.Request, pbK string) (bool, error) { - sign := r.Header.Get(common.ClientSignatureHeader) + sign := encryption.MiraclToHerumiSig(r.Header.Get(common.ClientSignatureHeader)) if len(sign) < 64 { return false, nil } @@ -707,7 +713,9 @@ func verifySignatureFromRequest(r *http.Request, pbK string) (bool, error) { return false, common.NewError("invalid_params", "Missing allocation tx") } - return encryption.Verify(pbK, sign, encryption.Hash(data)) + hash := encryption.Hash(data) + pbK = encryption.MiraclToHerumiPK(pbK) + return encryption.Verify(pbK, sign, hash) } // pathsFromReq retrieves paths value from request which can be represented as single "path" value or "paths" values, diff --git a/code/go/0chain.net/blobbercore/handler/zcncore.go b/code/go/0chain.net/blobbercore/handler/zcncore.go index 2966ee921..175889f82 100644 --- a/code/go/0chain.net/blobbercore/handler/zcncore.go +++ b/code/go/0chain.net/blobbercore/handler/zcncore.go @@ -2,6 +2,7 @@ package handler import ( "sync" + "encoding/json" "github.com/0chain/gosdk/core/common" "github.com/0chain/gosdk/zcncore" @@ -11,6 +12,7 @@ type ZCNStatus struct { wg *sync.WaitGroup success bool balance int64 + info string } func (zcn *ZCNStatus) OnBalanceAvailable(status int, value int64, info string) { @@ -23,6 +25,16 @@ func (zcn *ZCNStatus) OnBalanceAvailable(status int, value int64, info string) { zcn.balance = value } +func (zcn *ZCNStatus) OnInfoAvailable(op int, status int, info string, err string) { + defer zcn.wg.Done() + if status == zcncore.StatusSuccess { + zcn.success = true + } else { + zcn.success = false + } + zcn.info = info +} + func (zcn *ZCNStatus) OnTransactionComplete(t *zcncore.Transaction, status int) { defer zcn.wg.Done() if status == zcncore.StatusSuccess { @@ -49,7 +61,7 @@ func CheckBalance() (float64, error) { wg.Add(1) err := zcncore.GetBalance(statusBar) if err != nil { - return 0, common.NewError("check_balance_failed", "Call to GetBalance failed with err: "+err.Error()) + return 0, common.NewError("check_balance_failed", "Call to GetBalance failed with err: " + err.Error()) } wg.Wait() if !statusBar.success { @@ -58,6 +70,32 @@ func CheckBalance() (float64, error) { return zcncore.ConvertToToken(statusBar.balance), nil } +func GetBlobbers() ([]*zcncore.Blobber, error) { + var info struct { + Nodes []*zcncore.Blobber + } + + wg := &sync.WaitGroup{} + statusBar := &ZCNStatus{wg: wg} + wg.Add(1) + + err := zcncore.GetBlobbers(statusBar) + if err != nil { + return info.Nodes, common.NewError("get_blobbers_failed", "Call to GetBlobbers failed with err: " + err.Error()) + } + wg.Wait() + + if !statusBar.success { + return info.Nodes, nil + } + + if err = json.Unmarshal([]byte(statusBar.info), &info); err != nil { + return info.Nodes, common.NewError("get_blobbers_failed", "Decoding response to GetBlobbers failed with err: " + err.Error()) + } + + return info.Nodes, nil +} + func CallFaucet() error { wg := &sync.WaitGroup{} statusBar := &ZCNStatus{wg: wg} diff --git a/code/go/0chain.net/blobbercore/openapi/blobber.swagger.json b/code/go/0chain.net/blobbercore/openapi/blobber.swagger.json index 327425609..05beb9483 100644 --- a/code/go/0chain.net/blobbercore/openapi/blobber.swagger.json +++ b/code/go/0chain.net/blobbercore/openapi/blobber.swagger.json @@ -485,6 +485,12 @@ "OwnerPublicKey": { "type": "string" }, + "RepairerID": { + "type": "string" + }, + "PayerID": { + "type": "string" + }, "Expiration": { "type": "string", "format": "int64" @@ -521,9 +527,6 @@ "items": { "$ref": "#/definitions/v1Term" } - }, - "PayerID": { - "type": "string" } } }, diff --git a/code/go/0chain.net/blobbercore/readmarker/entity.go b/code/go/0chain.net/blobbercore/readmarker/entity.go index d01030834..a486ade2e 100644 --- a/code/go/0chain.net/blobbercore/readmarker/entity.go +++ b/code/go/0chain.net/blobbercore/readmarker/entity.go @@ -108,7 +108,6 @@ func GetLatestReadMarkerEntity(ctx context.Context, clientID string) (*ReadMarke } func SaveLatestReadMarker(ctx context.Context, rm *ReadMarker, isCreate bool) error { - var ( db = datastore.GetStore().GetTransaction(ctx) rmEntity = &ReadMarkerEntity{} diff --git a/code/go/0chain.net/blobbercore/stats/blobberstats.go b/code/go/0chain.net/blobbercore/stats/blobberstats.go index 080088a1b..5d4f1c111 100644 --- a/code/go/0chain.net/blobbercore/stats/blobberstats.go +++ b/code/go/0chain.net/blobbercore/stats/blobberstats.go @@ -42,7 +42,7 @@ type WriteMarkersStat struct { } type Stats struct { - TotalSize int64 `json:"total_size"` // the total allocated size + AllocatedSize int64 `json:"allocated_size"` UsedSize int64 `json:"used_size"` FilesSize int64 `json:"files_size"` ThumbnailsSize int64 `json:"thumbnails_size"` @@ -145,7 +145,7 @@ func (bs *BlobberStats) loadDetailedStats(ctx context.Context) { } given[as.AllocationID] = struct{}{} - bs.TotalSize += as.TotalSize + bs.AllocatedSize += as.AllocatedSize as.ReadMarkers, err = loadAllocReadMarkersStat(ctx, as.AllocationID) if err != nil { @@ -252,7 +252,7 @@ func (bs *BlobberStats) loadAllocationStats(ctx context.Context) { SUM(file_stats.num_of_block_downloads) as num_of_reads, SUM(reference_objects.num_of_blocks) as num_of_block_writes, COUNT(*) as num_of_writes, - allocations.size AS total_size, + allocations.size AS allocated_size, allocations.expiration_date AS expiration_date`). Joins(`INNER JOIN file_stats ON reference_objects.id = file_stats.ref_id`). @@ -262,6 +262,7 @@ func (bs *BlobberStats) loadAllocationStats(ctx context.Context) { Where(`reference_objects.type = 'f' AND reference_objects.deleted_at IS NULL`). Group(`reference_objects.allocation_id, allocations.expiration_date`). + Group(`reference_objects.allocation_id, allocations.size`). Rows() if err != nil { @@ -272,8 +273,8 @@ func (bs *BlobberStats) loadAllocationStats(ctx context.Context) { for rows.Next() { var as = &AllocationStats{} - err = rows.Scan(&as.AllocationID, &as.TotalSize, &as.FilesSize, &as.ThumbnailsSize, - &as.NumReads, &as.BlockWrites, &as.NumWrites, &as.Expiration) + err = rows.Scan(&as.AllocationID, &as.FilesSize, &as.ThumbnailsSize, + &as.NumReads, &as.BlockWrites, &as.NumWrites, &as.AllocatedSize, &as.Expiration) if err != nil { Logger.Error("Error in scanning record for blobber stats", zap.Error(err)) diff --git a/code/go/0chain.net/blobbercore/stats/handler.go b/code/go/0chain.net/blobbercore/stats/handler.go index aacd60049..faea3cdaa 100644 --- a/code/go/0chain.net/blobbercore/stats/handler.go +++ b/code/go/0chain.net/blobbercore/stats/handler.go @@ -65,7 +65,7 @@ const tpl = ` Allocated size (bytes) - {{ .TotalSize }} + {{ .AllocatedSize }} Used Size (bytes) diff --git a/code/go/0chain.net/core/common/handler.go b/code/go/0chain.net/core/common/handler.go index 71b933456..f84af02e6 100644 --- a/code/go/0chain.net/core/common/handler.go +++ b/code/go/0chain.net/core/common/handler.go @@ -39,6 +39,7 @@ type JSONReqResponderF func(ctx context.Context, json map[string]interface{}) (i /*Respond - respond either data or error as a response */ func Respond(w http.ResponseWriter, data interface{}, err error) { + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS for all. w.Header().Set("Content-Type", "application/json") if err != nil { data := make(map[string]interface{}, 2) @@ -105,6 +106,7 @@ func SetupCORSResponse(w http.ResponseWriter, r *http.Request) { */ func ToJSONResponse(handler JSONResponderF) ReqRespHandlerf { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS for all. if r.Method == "OPTIONS" { SetupCORSResponse(w, r) return diff --git a/code/go/0chain.net/core/encryption/keys.go b/code/go/0chain.net/core/encryption/keys.go index bda80893a..7000c2560 100644 --- a/code/go/0chain.net/core/encryption/keys.go +++ b/code/go/0chain.net/core/encryption/keys.go @@ -3,11 +3,14 @@ package encryption import ( "bufio" "io" + "strings" "0chain.net/core/common" "0chain.net/core/config" + . "0chain.net/core/logging" "github.com/0chain/gosdk/core/zcncrypto" + "github.com/herumi/bls-go-binary/bls" ) /*ReadKeys - reads a publicKey and a privateKey from a Reader. @@ -28,6 +31,8 @@ func ReadKeys(reader io.Reader) (publicKey string, privateKey string, publicIp s //Verify - given a public key and a signature and the hash used to create the signature, verify the signature func Verify(publicKey string, signature string, hash string) (bool, error) { + publicKey = MiraclToHerumiPK(publicKey) + signature = MiraclToHerumiSig(signature) signScheme := zcncrypto.NewSignatureScheme(config.Configuration.SignatureScheme) if signScheme != nil { err := signScheme.SetPublicKey(publicKey) @@ -38,3 +43,57 @@ func Verify(publicKey string, signature string, hash string) (bool, error) { } return false, common.NewError("invalid_signature_scheme", "Invalid signature scheme. Please check configuration") } + +// If input is normal herumi/bls public key, it returns it immmediately. +// So this is completely backward compatible with herumi/bls. +// If input is MIRACL public key, convert it to herumi/bls public key. +// +// This is an example of the raw public key we expect from MIRACL +var miraclExamplePK = `0418a02c6bd223ae0dfda1d2f9a3c81726ab436ce5e9d17c531ff0a385a13a0b491bdfed3a85690775ee35c61678957aaba7b1a1899438829f1dc94248d87ed36817f6dfafec19bfa87bf791a4d694f43fec227ae6f5a867490e30328cac05eaff039ac7dfc3364e851ebd2631ea6f1685609fc66d50223cc696cb59ff2fee47ac` +// +// This is an example of the same MIRACL public key serialized with ToString(). +// pk ([1bdfed3a85690775ee35c61678957aaba7b1a1899438829f1dc94248d87ed368,18a02c6bd223ae0dfda1d2f9a3c81726ab436ce5e9d17c531ff0a385a13a0b49],[039ac7dfc3364e851ebd2631ea6f1685609fc66d50223cc696cb59ff2fee47ac,17f6dfafec19bfa87bf791a4d694f43fec227ae6f5a867490e30328cac05eaff]) +func MiraclToHerumiPK(pk string) string { + if len(pk) != len(miraclExamplePK) { + // If input is normal herumi/bls public key, it returns it immmediately. + return pk + } + n1 := pk[2:66] + n2 := pk[66:(66+64)] + n3 := pk[(66+64):(66+64+64)] + n4 := pk[(66+64+64):(66+64+64+64)] + var p bls.PublicKey + err := p.SetHexString("1 " + n2 + " " + n1 + " " + n4 + " " + n3) + if err != nil { + Logger.Error("MiraclToHerumiPK: " + err.Error()) + } + return p.SerializeToHexStr() +} + +// Converts signature 'sig' to format that the herumi/bls library likes. +// zwallets are using MIRACL library which send a MIRACL signature not herumi +// lib. +// +// If the 'sig' was not in MIRACL format, we just return the original sig. +const miraclExampleSig = `(0d4dbad6d2586d5e01b6b7fbad77e4adfa81212c52b4a0b885e19c58e0944764,110061aa16d5ba36eef0ad4503be346908d3513c0a2aedfd0d2923411b420eca)` +func MiraclToHerumiSig(sig string) string { + if len(sig) <= 2 { + return sig + } + if sig[0] != miraclExampleSig[0] { + return sig + } + withoutParens := sig[1: (len(sig)-1) ] + comma := strings.Index(withoutParens, ",") + if comma < 0 { + return "00" + } + n1 := withoutParens[0:comma] + n2 := withoutParens[(comma+1):] + var sign bls.Sign + err := sign.SetHexString("1 " + n1 + " " + n2) + if err != nil { + Logger.Error("MiraclToHerumiSig: " + err.Error()) + } + return sign.SerializeToHexStr() +} diff --git a/code/go/0chain.net/core/encryption/keys_test.go b/code/go/0chain.net/core/encryption/keys_test.go index c8ee3d092..592dbe4e8 100644 --- a/code/go/0chain.net/core/encryption/keys_test.go +++ b/code/go/0chain.net/core/encryption/keys_test.go @@ -1,8 +1,10 @@ package encryption import ( + "encoding/hex" "github.com/0chain/gosdk/zboxcore/client" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" ) @@ -20,4 +22,58 @@ func TestSignatureVerify(t *testing.T) { ) assert.Nil(t, err) assert.Equal(t, res, true) +} + +func TestMiraclToHerumiPK(t *testing.T) { + miraclpk1 := `0418a02c6bd223ae0dfda1d2f9a3c81726ab436ce5e9d17c531ff0a385a13a0b491bdfed3a85690775ee35c61678957aaba7b1a1899438829f1dc94248d87ed36817f6dfafec19bfa87bf791a4d694f43fec227ae6f5a867490e30328cac05eaff039ac7dfc3364e851ebd2631ea6f1685609fc66d50223cc696cb59ff2fee47ac` + pk1 := MiraclToHerumiPK(miraclpk1) + + require.EqualValues(t, pk1, "68d37ed84842c91d9f82389489a1b1a7ab7a957816c635ee750769853aeddf1b490b3aa185a3f01f537cd1e9e56c43ab2617c8a3f9d2a1fd0dae23d26b2ca018") + + // Assert DeserializeHexStr works on the output of MiraclToHerumiPK + var pk bls.PublicKey + err := pk.DeserializeHexStr(pk1) + require.NoError(t, err) +} + +func TestMiraclToHerumiSig(t *testing.T) { + miraclsig1 := `(0d4dbad6d2586d5e01b6b7fbad77e4adfa81212c52b4a0b885e19c58e0944764,110061aa16d5ba36eef0ad4503be346908d3513c0a2aedfd0d2923411b420eca)` + sig1 := MiraclToHerumiSig(miraclsig1) + + require.EqualValues(t, sig1, "644794e0589ce185b8a0b4522c2181faade477adfbb7b6015e6d58d2d6ba4d0d") + + // Assert DeserializeHexStr works on the output of MiraclToHerumiSig + var sig bls.Sign + err := sig.DeserializeHexStr(sig1) + require.NoError(t, err) + + // Test that passing in normal herumi sig just gets back the original. + sig2 := MiraclToHerumiSig(sig1) + if sig1 != sig2 { + panic("Signatures should be the same.") + } +} + +// Helper code to print out expected values of Hash and conversion functions. +func TestDebugOnly(t *testing.T) { + + // clientKey := "536d2ecfe5aab6c343e8c2e7ee9daa60c43eecc53f4b1c07a6cb2648d9e66c14f2e3fcd43875be40722992f56570fe3c751caacbc7d859b309c787f654bd5a97" + // // => 5c2fdfa03fc013cff0e4b716f0529b914e18fd2bc6cdfed49df13b6e3dc4684d + + clientKey := "0416c528570ce46eb83584cd604a9ed62644ef4f71a86587d57e4ab91953ff4699107374870799ad4550c4f3833cca2a4d5de75436d67caf89097f1e7d6d7de6d424cb5a08b9dca8957ea7c81a23d066b93a27500954cd29733149ec1f8a8abd540d08f9f81bb24b83ff27e24f173e639573e10a22ed7b0ca326a1aa9dc03e1eef" + // => bd3adcacc78ed4352931b138729986a07d2bf0e0a3bf2c885b37a9a0e649dd87 + // Looking for bd3adcacc78ed4352931b138729986a07d2bf0e0a3bf2c885b37a9a0e649dd87 + + clientKeyBytes, _ := hex.DecodeString(clientKey) + h := Hash(clientKeyBytes) + + fmt.Println("hash ", h) + + herumipk := MiraclToHerumiPK(clientKey) + fmt.Println("herumipk ", herumipk) + clientKeyBytes2, _ := hex.DecodeString(herumipk) + h = Hash(clientKeyBytes2) + fmt.Println("hash2 ", h) + + } \ No newline at end of file diff --git a/code/go/0chain.net/core/transaction/entity.go b/code/go/0chain.net/core/transaction/entity.go index 72f8eabe5..8be9e6c31 100644 --- a/code/go/0chain.net/core/transaction/entity.go +++ b/code/go/0chain.net/core/transaction/entity.go @@ -68,13 +68,19 @@ type StakePoolSettings struct { ServiceCharge float64 `json:"service_charge"` } +type StorageNodeGeolocation struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + type StorageNode struct { - ID string `json:"id"` - BaseURL string `json:"url"` - Terms Terms `json:"terms"` - Capacity int64 `json:"capacity"` - PublicKey string `json:"-"` - StakePoolSettings StakePoolSettings `json:"stake_pool_settings"` + ID string `json:"id"` + BaseURL string `json:"url"` + Geolocation StorageNodeGeolocation `json:"geolocation"` + Terms Terms `json:"terms"` + Capacity int64 `json:"capacity"` + PublicKey string `json:"-"` + StakePoolSettings StakePoolSettings `json:"stake_pool_settings"` } type BlobberAllocation struct { @@ -95,6 +101,7 @@ type StorageAllocation struct { Finalized bool `json:"finalized"` CCT time.Duration `json:"challenge_completion_time"` TimeUnit time.Duration `json:"time_unit"` + IsImmutable bool `json:"is_immutable"` } func (sa *StorageAllocation) Until() common.Timestamp { @@ -115,7 +122,6 @@ const ( READ_REDEEM = "read_redeem" CHALLENGE_RESPONSE = "challenge_response" BLOBBER_HEALTH_CHECK = "blobber_health_check" - UPDATE_BLOBBER_SETTINGS = "update_blobber_settings" FINALIZE_ALLOCATION = "finalize_allocation" ) diff --git a/code/go/0chain.net/go.mod b/code/go/0chain.net/go.mod index ef3b0dd34..8bdf88b70 100644 --- a/code/go/0chain.net/go.mod +++ b/code/go/0chain.net/go.mod @@ -9,6 +9,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0 + github.com/herumi/bls-go-binary v0.0.0-20191119080710-898950e1a520 // indirect github.com/jackc/pgproto3/v2 v2.0.4 // indirect github.com/koding/cache v0.0.0-20161222233015-e8a81b0b3f20 github.com/minio/minio-go v6.0.14+incompatible diff --git a/code/go/0chain.net/validatorcore/storage/context.go b/code/go/0chain.net/validatorcore/storage/context.go index 79a977074..2a761fe4a 100644 --- a/code/go/0chain.net/validatorcore/storage/context.go +++ b/code/go/0chain.net/validatorcore/storage/context.go @@ -10,7 +10,8 @@ import ( func SetupContext(handler common.JSONResponderF) common.JSONResponderF { return func(ctx context.Context, r *http.Request) (interface{}, error) { ctx = context.WithValue(ctx, CLIENT_CONTEXT_KEY, r.Header.Get(common.ClientHeader)) - ctx = context.WithValue(ctx, CLIENT_KEY_CONTEXT_KEY, r.Header.Get(common.ClientKeyHeader)) + ctx = context.WithValue(ctx, CLIENT_KEY_CONTEXT_KEY, + r.Header.Get(common.ClientKeyHeader)) res, err := handler(ctx, r) return res, err } diff --git a/config/0chain_blobber.yaml b/config/0chain_blobber.yaml index 73201116e..70a5fbc51 100755 --- a/config/0chain_blobber.yaml +++ b/config/0chain_blobber.yaml @@ -87,6 +87,10 @@ db: host: postgres port: 5432 +geolocation: + latitude: 0 + longitude: 0 + minio: # Enable or disable minio backup service start: false diff --git a/docker.local/b0docker-compose.yml b/docker.local/b0docker-compose.yml index 6030b1930..af8ba85c1 100644 --- a/docker.local/b0docker-compose.yml +++ b/docker.local/b0docker-compose.yml @@ -62,7 +62,7 @@ services: ports: - "505${BLOBBER}:505${BLOBBER}" - "703${BLOBBER}:703${BLOBBER}" - command: ./bin/blobber --port 505${BLOBBER} --grpc_port 703${BLOBBER} --hostname localhost --deployment_mode 0 --keys_file keysconfig/b0bnode${BLOBBER}_keys.txt --files_dir /blobber/files --log_dir /blobber/log --db_dir /blobber/data --minio_file keysconfig/minio_config.txt + command: ./bin/blobber --port 505${BLOBBER} --grpc_port 703${BLOBBER} --hostname 198.18.0.9${BLOBBER} --deployment_mode 0 --keys_file keysconfig/b0bnode${BLOBBER}_keys.txt --files_dir /blobber/files --log_dir /blobber/log --db_dir /blobber/data --minio_file keysconfig/minio_config.txt networks: default: testnet0: diff --git a/docker.local/bin/build.blobber-integration-tests.sh b/docker.local/bin/build.blobber-integration-tests.sh index 69b76eff7..895d5974c 100755 --- a/docker.local/bin/build.blobber-integration-tests.sh +++ b/docker.local/bin/build.blobber-integration-tests.sh @@ -4,8 +4,21 @@ set -e GIT_COMMIT=$(git rev-list -1 HEAD) echo $GIT_COMMIT -docker build --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/ValidatorDockerfile . -t validator -docker build --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/IntegrationTestsBlobberDockerfile . -t blobber +cmd="build" + +for arg in "$@" +do + case $arg in + -m1|--m1|m1) + echo "The build will be performed for Apple M1 chip" + cmd="buildx build --platform linux/amd64" + shift + ;; + esac +done + +docker $cmd --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/ValidatorDockerfile . -t validator +docker $cmd --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/IntegrationTestsBlobberDockerfile . -t blobber for i in $(seq 1 6); do diff --git a/docker.local/bin/build.blobber.sh b/docker.local/bin/build.blobber.sh index 6b27f854c..4cd4fda82 100755 --- a/docker.local/bin/build.blobber.sh +++ b/docker.local/bin/build.blobber.sh @@ -4,8 +4,21 @@ set -e GIT_COMMIT=$(git rev-list -1 HEAD) echo $GIT_COMMIT -docker build --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/ValidatorDockerfile . -t validator -docker build --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/Dockerfile . -t blobber +cmd="build" + +for arg in "$@" +do + case $arg in + -m1|--m1|m1) + echo "The build will be performed for Apple M1 chip" + cmd="buildx build --platform linux/amd64" + shift + ;; + esac +done + +docker $cmd --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/ValidatorDockerfile . -t validator +docker $cmd --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/Dockerfile . -t blobber for i in $(seq 1 6); do diff --git a/docs/cicd/CICD_GITACTIONS.md b/docs/cicd/CICD_GITACTIONS.md new file mode 100644 index 000000000..3aecae196 --- /dev/null +++ b/docs/cicd/CICD_GITACTIONS.md @@ -0,0 +1,128 @@ + + +## Guide to CI/CD using github actions + +## Workflow Creation. + - A new workflow is created using Go project with the file name called "build.yml". + - By default the path of build.yml is ".github/workflows.build.yml" + - Completed or running CI/CD can be seen under actions option. + + +## Details of components being used in build.yml. +#### Workflow name +Here the name of the workflow is defined i.e. "Dockerize" +``` +name: Dockerize +``` + +#### Input Option to trigger manually builds +To run the workflow using manual option, *work_dispatch* is used. Which will ask for the input to tigger the builds with *latest* tag or not. If we select for **yes**, image will be build with *latest* tag as well as with *branch-commitid* tag. But if we select for **no**, image will be build with *branch-commitid* tag only. + +``` +on: + workflow_dispatch: + inputs: + latest_tag: + description: 'type yes for building latest tag' + default: 'no' + required: true +``` + +#### Global ENV setup +Environment variable is defined with the secrets added to the repository. Here secrets contains the docker images(example- dockerhub) repository name. +``` +env: + BLOBBER_REGISTRY: ${{ secrets.BLOBBER_REGISTRY }} + VALIDATOR_REGISTRY: ${{ secrets.VALIDATOR_REGISTRY }} +``` + +#### Defining jobs and runner +Jobs are defined which contains the various steps for creating and pushing the builds. Runner envionment is also defined used for making the builds. +``` +jobs: + dockerize_blobber: + runs-on: ubuntu-20.04 + ... + dockerize_validator: + runs-on: ubuntu-20.04 +``` + +#### Different steps used in creating the builds +Here different steps are defined used for creating the builds. + - *uses* --> checkout to branch from what code to create the builds. + - *Get the version* --> Creating the tags by combining the branch name & first 8 digits of commit id. + - *Login to Docker Hub* --> Logging into the docker hub using Username and Password from secrets of the repository. + - *Build blobber/validator* --> Building, tagging and pushing the docker images with the *Get the version* tag. + - *Push blobber/validator* --> Here we are checking if the input given by user is **yes**, images is also pushed with latest tag also. + +For Blobber +``` +steps: +- uses: actions/checkout@v2 + +- name: Get the version + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} + +- name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + +- name: Build blobber + run: | + docker build -t $BLOBBER_REGISTRY:$TAG -f "$DOCKERFILE_BLOB" . + docker tag $BLOBBER_REGISTRY:$TAG $BLOBBER_REGISTRY:latest + ocker push $BLOBBER_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_BLOB: "docker.local/Dockerfile" + +- name: Push blobber + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $BLOBBER_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} +``` +For Validator +``` +steps: +- uses: actions/checkout@v1 + +- name: Get the version + id: get_version + run: | + BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g') + SHORT_SHA=$(echo $GITHUB_SHA | head -c 8) + echo ::set-output name=BRANCH::${BRANCH} + echo ::set-output name=VERSION::${BRANCH}-${SHORT_SHA} +- name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + +- name: Build validator + run: | + docker build -t $VALIDATOR_REGISTRY:$TAG -f "$DOCKERFILE_PROXY" . + docker tag $VALIDATOR_REGISTRY:$TAG $VALIDATOR_REGISTRY:latest + docker push $VALIDATOR_REGISTRY:$TAG + env: + TAG: ${{ steps.get_version.outputs.VERSION }} + DOCKERFILE_PROXY: "docker.local/ValidatorDockerfile" + +- name: Push validator + run: | + if [[ "$PUSH_LATEST" == "yes" ]]; then + docker push $VALIDATOR_REGISTRY:latest + fi + env: + PUSH_LATEST: ${{ github.event.inputs.latest_tag }} +``` diff --git a/docs/cicd/blobber.png b/docs/cicd/blobber.png new file mode 100644 index 0000000000000000000000000000000000000000..14364278ec5d1f0b5dcb8850281288390f76b2ff GIT binary patch literal 54031 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 qzJCMv9f5=YdB@22U`LEb4v4s~ah$dB0;IZ literal 0 HcmV?d00001 diff --git a/docs/src/repair.plantuml b/docs/src/repair.plantuml index 94009b9b9..ffde5ea64 100644 --- a/docs/src/repair.plantuml +++ b/docs/src/repair.plantuml @@ -1,6 +1,6 @@ @startuml actor Client -actor Payer +actor Repairer group partial upload Client -> Blobber : List command on a directory diff --git a/sql/14-increase_owner_pubkey.sql b/sql/14-increase_owner_pubkey.sql new file mode 100644 index 000000000..f6cd40520 --- /dev/null +++ b/sql/14-increase_owner_pubkey.sql @@ -0,0 +1,16 @@ +-- +-- Increase the char limit of owner_public_key from 256 to 512. +-- + +-- pew-pew +\connect blobber_meta; + +-- in a transaction +BEGIN; + ALTER TABLE allocations + ALTER COLUMN owner_public_key TYPE varchar(512); + ALTER TABLE read_markers + ALTER COLUMN client_public_key TYPE varchar(512); + ALTER TABLE write_markers + ALTER COLUMN client_key TYPE varchar(512); +COMMIT; diff --git a/sql/15-add-allocation-columns.sql b/sql/15-add-allocation-columns.sql new file mode 100644 index 000000000..c983cf73f --- /dev/null +++ b/sql/15-add-allocation-columns.sql @@ -0,0 +1,6 @@ +\connect blobber_meta; + +BEGIN; + ALTER TABLE allocations ADD COLUMN repairer_id VARCHAR(64) NOT NULL; + ALTER TABLE allocations ADD COLUMN is_immutable BOOLEAN NOT NULL; +COMMIT; \ No newline at end of file From 0acfdc720742fa908c3d76009a28882a26cfbf14 Mon Sep 17 00:00:00 2001 From: Uk Date: Tue, 6 Jul 2021 23:01:32 +0545 Subject: [PATCH 2/4] Replace gosdk version --- code/go/0chain.net/go.mod | 4 ++-- code/go/0chain.net/go.sum | 17 ----------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/code/go/0chain.net/go.mod b/code/go/0chain.net/go.mod index 8bdf88b70..f6e01cca6 100644 --- a/code/go/0chain.net/go.mod +++ b/code/go/0chain.net/go.mod @@ -1,7 +1,7 @@ module 0chain.net require ( - github.com/0chain/gosdk v1.1.6 + github.com/0chain/gosdk marketplace github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/didip/tollbooth v4.0.2+incompatible github.com/go-ini/ini v1.55.0 // indirect @@ -9,7 +9,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0 - github.com/herumi/bls-go-binary v0.0.0-20191119080710-898950e1a520 // indirect + github.com/herumi/bls-go-binary v0.0.0-20191119080710-898950e1a520 github.com/jackc/pgproto3/v2 v2.0.4 // indirect github.com/koding/cache v0.0.0-20161222233015-e8a81b0b3f20 github.com/minio/minio-go v6.0.14+incompatible diff --git a/code/go/0chain.net/go.sum b/code/go/0chain.net/go.sum index a0ecd3327..dec840020 100644 --- a/code/go/0chain.net/go.sum +++ b/code/go/0chain.net/go.sum @@ -334,7 +334,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -350,7 +349,6 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -400,7 +398,6 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v1.0.1-0.20201006035406-b97b5ead31f7/go.mod h1:yk5b0mALVusDL5fMM6Rd1wgnoO5jUPhwsQ6LQAJTidQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -412,7 +409,6 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -446,22 +442,18 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= @@ -475,7 +467,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -500,7 +491,6 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -513,7 +503,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd h1:ePuNC7PZ6O5BzgPn9bZayERXBdfZjUYoXEf5BTfDfh8= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= @@ -614,7 +603,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= @@ -657,7 +645,6 @@ golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -681,7 +668,6 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -778,7 +764,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -790,7 +775,6 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -818,7 +802,6 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= From fbc9fd77ddd6c4b52d3b7ff3695faafe99a6650f Mon Sep 17 00:00:00 2001 From: Uk Date: Wed, 7 Jul 2021 18:17:57 +0545 Subject: [PATCH 3/4] Update sql version --- ...{14-add-marketplace-table.sql => 16-add-marketplace-table.sql} | 0 ...erence-objects.sql => 17-add-indexes-to-reference-objects.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename sql/{14-add-marketplace-table.sql => 16-add-marketplace-table.sql} (100%) rename sql/{15-add-indexes-to-reference-objects.sql => 17-add-indexes-to-reference-objects.sql} (100%) diff --git a/sql/14-add-marketplace-table.sql b/sql/16-add-marketplace-table.sql similarity index 100% rename from sql/14-add-marketplace-table.sql rename to sql/16-add-marketplace-table.sql diff --git a/sql/15-add-indexes-to-reference-objects.sql b/sql/17-add-indexes-to-reference-objects.sql similarity index 100% rename from sql/15-add-indexes-to-reference-objects.sql rename to sql/17-add-indexes-to-reference-objects.sql From 4518c892bee85e20fb4f929538c7ffc8bdd19014 Mon Sep 17 00:00:00 2001 From: Uk Date: Wed, 7 Jul 2021 19:27:29 +0545 Subject: [PATCH 4/4] Force authticket check when provided --- .../0chain.net/blobbercore/handler/object_operation_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go index 5f43e3946..8db11078e 100644 --- a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go +++ b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go @@ -293,7 +293,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) ( var authToken *readmarker.AuthTicket = nil - if !isOwner && !isRepairer && !isCollaborator { + if (!isOwner && !isRepairer && !isCollaborator) || len(r.FormValue("auth_token")) > 0 { var authTokenString = r.FormValue("auth_token") // check auth token