diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..475e44cb3 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# This is an example .env file with default passwords and private keys. +# Do not use this in production or with any public-facing ports! +DELA_PROXY_URL=http://172.19.44.254:8080 +DATABASE_USERNAME=dvoting +DATABASE_PASSWORD=Ohw0phoa # choose any PostgreSQL password +DATABASE_HOST=db +DATABASE_PORT=5432 +DB_PATH=dvoting # LMDB database path +FRONT_END_URL=http://127.0.0.1:3000 # the automated frontend tests expect this value do not change it +BACKEND_HOST=backend +BACKEND_PORT=5000 +SESSION_SECRET=kaibaaF9 # choose any secret +# For public-facing services and production, this key needs to be changed! +PRIVATE_KEY=6aadf480d068ac896330b726802abd0da2a5f3824f791fe8dbd4cd555e80b809 +PUBLIC_KEY=3e5fcaed4c5d79a8eccceeb087ee0a13b8f91d917ed62017a9cd28e13b228389 +PROXYPORT=8080 +NODEPORT=2000 # DELA node port + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..46f272ec8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +Thank you for opening a pull request with this project, please also: + +* [ ] add a brief description of your changes here +* [ ] assign the PR to yourself, or to the person(s) working on it +* [ ] start in `draft` mode and `in progress` pipeline in the project (if applicable) +* [ ] if applicable, add this PR to its related issue by one of the special keywords [Closing keywords](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue) +* once it's ready + * [ ] put it in the `Review` or `Ready4Merge` pipeline in the project (if applicable) + * [ ] remove `draft` + * [ ] assign a reviewer diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 000000000..a8336a539 --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,55 @@ +name: Build docker + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + build-docker: + name: Build D-Voting Docker images + runs-on: ubuntu-22.04 + if: '! github.event.pull_request.draft' + env: + DockerTag: ${{ (github.ref == 'refs/heads/main') && 'latest' || github.head_ref }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Login to GHCR + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Frontend + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfiles/Dockerfile.frontend + platforms: linux/amd64 + push: true + tags: ghcr.io/c4dt/d-voting-frontend:${{ env.DockerTag }} + - name: Build Backend + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfiles/Dockerfile.backend + platforms: linux/amd64 + push: true + tags: ghcr.io/c4dt/d-voting-backend:${{ env.DockerTag }} + - name: Build D-Voting + uses: docker/build-push-action@v2 + with: + context: . + target: build + file: Dockerfiles/Dockerfile.dela + platforms: linux/amd64 + push: true + tags: ghcr.io/c4dt/d-voting-dela:${{ env.DockerTag }} diff --git a/.github/workflows/go_dvoting_test.yml b/.github/workflows/go_dvoting_test.yml index 5deea0ce2..f98569577 100644 --- a/.github/workflows/go_dvoting_test.yml +++ b/.github/workflows/go_dvoting_test.yml @@ -19,7 +19,7 @@ jobs: - name: Install crypto util from Dela run: | - git clone https://github.com/dedis/dela.git + git clone https://github.com/c4dt/dela.git cd dela go install ./cli/crypto diff --git a/.github/workflows/go_scenario_test.yml b/.github/workflows/go_scenario_test.yml index 942b161d1..c2e609697 100644 --- a/.github/workflows/go_scenario_test.yml +++ b/.github/workflows/go_scenario_test.yml @@ -18,7 +18,7 @@ jobs: - name: Install crypto util from Dela run: | - git clone https://github.com/dedis/dela.git + git clone https://github.com/c4dt/dela.git cd dela go install ./cli/crypto diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index 6392aed7f..dba5a8188 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -27,35 +27,4 @@ jobs: - name: Test all, except integration, with coverage run: | - go test -json -covermode=count -coverprofile=profile.cov $(go list ./... | grep -v /integration) 2>&1 | tee report.json - - - name: Sonarcloud scan - uses: sonarsource/sonarcloud-github-action@master - with: - args: > - -Dsonar.organization=dedis - -Dsonar.projectKey=dedis_d-voting - -Dsonar.go.tests.reportPaths=report.json - -Dsonar.go.coverage.reportPaths=profile.cov - -Dsonar.coverage.exclusions=**/*_test.go,/internal/**/* - -Dsonar.issue.ignore.multicriteria=e1 - -Dsonar.issue.ignore.multicriteria.e1.ruleKey=*Naming* - -Dsonar.issue.ignore.multicriteria.e1.resourceKey=**/*_test.go - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - - name: Send coverage - uses: shogo82148/actions-goveralls@v1 - with: - path-to-profile: profile.cov - parallel: true - - # notifies that all test jobs are finished. - finish: - needs: test - runs-on: ubuntu-latest - steps: - - uses: shogo82148/actions-goveralls@v1 - with: - parallel-finished: true + go test $(go list ./... | grep -v /integration) diff --git a/.gitignore b/.gitignore index a6b66c431..361116e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,9 @@ nodedata/ deb-package/dist/** +.env + +dela/ +bin/ +nodes/ +cookies.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..16e1cbfa2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Latest changes in each category go to the top + +## [Unreleased] + +### Added +- dev_login can change userId when clicking on the user in the upper right +- admin can now add users as voters +- New debugging variables in [local_vars.sh](./scripts/local_vars.sh) +- Changelog - please use it + +### Changed +- for the Dockerfiles and docker-compose.yml, `DELA_NODE_URL` has been replaced with `DELA_PROXY_URL`, + which is the more accurate name. +- the actions in package.json for the frontend changed. Both are somewhat development mode, + as the webserver is not supposed to be used in production. + - `start`: starts in plain mode + - `start-https`: starts in HTTPS mode + +### Deprecated +### Removed +### Fixed +- Proxy editing fixed: adding, modifying, deleting now works +- When fetching form and user updates, only do it when showing the activity +- Redirection when form doesn't exist and nicer error message +- File formatting and errors in comments +- Popup when voting and some voting translation fixes +- Fixed return error when voting + +### Security +- Use `REACT_APP_RANDOMIZE_VOTE_ID === 'true'` to indicate randomizing vote ids diff --git a/Dockerfiles/Dockerfile.backend b/Dockerfiles/Dockerfile.backend index 4db0ec75e..a87b4ede3 100644 --- a/Dockerfiles/Dockerfile.backend +++ b/Dockerfiles/Dockerfile.backend @@ -3,4 +3,5 @@ FROM node:20-bookworm WORKDIR /web/backend COPY ../web/backend . RUN npm install -ENTRYPOINT ["/bin/bash", "-c", "npm start"] +ENTRYPOINT ["npm"] +CMD ["start"] diff --git a/Dockerfiles/Dockerfile.dela b/Dockerfiles/Dockerfile.dela index 27f4fc909..5535c8dbd 100644 --- a/Dockerfiles/Dockerfile.dela +++ b/Dockerfiles/Dockerfile.dela @@ -1,15 +1,17 @@ -FROM golang:1.20.6-bookworm - +FROM golang:1.20.6-bookworm AS base RUN apt-get update && apt-get install git # make sure we're using the same head as d-voting -RUN git clone -b fix-bbolt https://github.com/dedis/dela.git +RUN git clone https://github.com/c4dt/dela.git WORKDIR /go/dela/cli/crypto RUN go install WORKDIR /go/d-voting COPY . . + +FROM base AS build +COPY --from=base /go/dela . +COPY --from=base /go/d-voting . WORKDIR /go/d-voting/cli/dvoting RUN go build ENV PATH=/go/dela/cli/crypto:/go/d-voting/cli/dvoting:${PATH} -WORKDIR /go -ENTRYPOINT ["/bin/bash", "-c", "dvoting --config /data/node start --postinstall --proxyaddr :$PROXYPORT --proxykey $PROXYKEY --listen tcp://0.0.0.0:2000 --public http://$HOSTNAME:$NODEPORT --routing tree"] +ENTRYPOINT ["/bin/bash", "-c", "dvoting --config /data/node start --postinstall --proxyaddr :$PROXYPORT --proxykey $PROXYKEY --listen tcp://0.0.0.0:2000 --public $PUBLIC_URL --routing tree --noTLS"] CMD [] diff --git a/Dockerfiles/Dockerfile.dela.debug b/Dockerfiles/Dockerfile.dela.debug deleted file mode 100644 index 47cee9b89..000000000 --- a/Dockerfiles/Dockerfile.dela.debug +++ /dev/null @@ -1,18 +0,0 @@ -FROM golang:1.20.6-bookworm - -# https://blog.jetbrains.com/go/2020/05/06/debugging-a-go-application-inside-a-docker-container/ -RUN go install github.com/go-delve/delve/cmd/dlv@latest - -RUN apt-get update && apt-get install git -RUN git clone https://github.com/dedis/dela.git -RUN git clone https://github.com/dedis/d-voting.git -WORKDIR /go/dela/cli/crypto -RUN go install -WORKDIR /go/d-voting/cli/dvoting - -RUN go build -gcflags="all=-N -l" - -ENV PATH=/go/dela/cli/crypto:/go/d-voting/cli/dvoting:${PATH} -WORKDIR /go -ENTRYPOINT ["/bin/bash", "-c", "dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient exec /go/d-voting/cli/dvoting/dvoting -- --config /data/node start --postinstall --proxyaddr :$PROXYPORT --proxykey $PROXYKEY --listen tcp://0.0.0.0:2000 --public http://$HOSTNAME:2000 --routing tree"] -CMD [] diff --git a/Dockerfiles/Dockerfile.frontend b/Dockerfiles/Dockerfile.frontend index ab04b319a..77376ff36 100644 --- a/Dockerfiles/Dockerfile.frontend +++ b/Dockerfiles/Dockerfile.frontend @@ -5,4 +5,5 @@ ENV REACT_APP_NOMOCK=on WORKDIR /web/frontend COPY ../web/frontend . RUN npm install -ENTRYPOINT ["npm", "start"] +ENTRYPOINT ["npm"] +CMD ["start"] diff --git a/Makefile b/Makefile index a774d69a0..a7eb71f25 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ version=$(shell git describe --abbrev=0 --tags || echo '0.0.0') -versionFlag="github.com/dedis/d-voting.Version=$(version)" +versionFlag="github.com/c4dt/d-voting.Version=$(version)" versionFile=$(shell echo $(version) | tr . _) -timeFlag="github.com/dedis/d-voting.BuildTime=$(shell date +'%d/%m/%y_%H:%M')" +timeFlag="github.com/c4dt/d-voting.BuildTime=$(shell date +'%d/%m/%y_%H:%M')" lint: # Coding style static check. diff --git a/README.docker.md b/README.docker.md index 230fbc875..23f8ff29e 100644 --- a/README.docker.md +++ b/README.docker.md @@ -15,13 +15,13 @@ when running `docker compose`. The environment file needs to contain ``` -DELA_NODE_URL=http://172.19.44.254:8080 +DELA_PROXY_URL=http://172.19.44.254:8080 DATABASE_USERNAME=dvoting DATABASE_PASSWORD=XXX # choose any PostgreSQL password DATABASE_HOST=db DATABASE_PORT=5432 DB_PATH=dvoting # LMDB database path -FRONT_END_URL=http://127.0.0.1:3000 +FRONT_END_URL=http://127.0.0.1:3000 # the automated frontend tests expect this value do not change it BACKEND_HOST=backend BACKEND_PORT=5000 SESSION_SECRET=XXX # choose any secret @@ -31,31 +31,39 @@ PROXYPORT=8080 NODEPORT=2000 # DELA node port ``` +For the `PUBLIC_KEY` and `PRIVATE_KEY`, you need to run the following commands: + +```bash +cd web/backend +npm ci +npm run keygen +``` + +And then copy the two lines to the `.env` file. + There are two Docker Compose file you may use: -* `docker-compose/docker-compose.yml` for the currently released version, or +* `docker-compose/docker-compose.yml` for the preprod version, or * `docker-compose/docker-compose.debug.yml` for the development/debugging version -You can either run +You run ``` export COMPOSE_FILE= ``` -or pass the `-f/--file ` argument to choose between -the files. - -Using the currently released version will pull the images from the GitHub container registry. - -If you instead use the development/debugging version the images will be build locally and you can debug your developments. +The preprod version will create an environment without any debugging tools that's as close as possible to a real environment. +It is meant to be used to test the `main` branch before deploying it to production. Use the development/debugging version +for setting up your local development environment. Run ``` +docker compose build docker compose up ``` -(possibly with the `-f/--file` argument) to set up the environment. +to set up the environment. /!\ Any subsequent `docker compose` commands must be run with `COMPOSE_FILE` being set to the Docker Compose file that defines the current environment. diff --git a/README.md b/README.md index e94a54b3a..635f11808 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - - + +
- + GitHub contributors - + GitHub release (latest SemVer) @@ -26,37 +26,37 @@ Blockchain - - + + - - + + - - + + - - + +
- - + + - - Go Reference + + Go Reference WEB - - + + - - + + @@ -67,7 +67,7 @@ # D-Voting **D-Voting** is an e-voting platform based on the -[Dela](https://github.com/dedis/dela) blockchain. It uses state-of-the-art +[Dela](https://github.com/c4dt/dela) blockchain. It uses state-of-the-art protocols that guarantee privacy of votes and a fully decentralized process. This project was born in early 2021 and has been iteratively implemented by EPFL students under the supervision of DEDIS members. @@ -147,7 +147,7 @@ sometimes refer to the blockchain node simply as a "node". The following component diagrams summarizes the interaction between those high-level components: -[minogrpc]: https://github.com/dedis/dela/tree/master/mino/minogrpc +[minogrpc]: https://github.com/c4dt/dela/tree/master/mino/minogrpc ![Global component diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/dedis/d-voting/main/docs/assets/component-global.puml) @@ -390,7 +390,7 @@ results. 2: Install the `crypto` utility from Dela: ```sh -git clone https://github.com/dedis/dela.git +git clone https://github.com/c4dt/dela.git cd dela/cli/crypto go install ``` @@ -601,7 +601,7 @@ Afterwards use the following commands, replace 4 by the desired nb of nodes : ./runNode.sh -n 4 -a true -d true ./setupnNode.sh -n 4 -d true -NNODES=4 KILLNODE=true go test -v -run ^TestScenario$ github.com/dedis/d-voting/integration -count=1 +NNODES=4 KILLNODE=true go test -v -run ^TestScenario$ github.com/c4dt/d-voting/integration -count=1 ``` Here we set KILLNODE=true or false to decide whether kill and restart a node @@ -650,8 +650,8 @@ Build info can be added to the binary with the `ldflags`, at build time. Infos are stored on variables in the root `mod.go`. For example: ```sh -versionFlag="github.com/dedis/d-voting.Version=`git describe --tags`" -timeFlag="github.com/dedis/d-voting.BuildTime=`date +'%d/%m/%y_%H:%M'`" +versionFlag="github.com/c4dt/d-voting.Version=`git describe --tags`" +timeFlag="github.com/c4dt/d-voting.BuildTime=`date +'%d/%m/%y_%H:%M'`" go build -ldflags="-X $versionFlag -X $timeFlag" ./cli/dvoting ``` diff --git a/autotest.sh b/autotest.sh index b9872287b..67ef14ec1 100755 --- a/autotest.sh +++ b/autotest.sh @@ -52,7 +52,7 @@ do ./setupnNode.sh -n $N_NODE -d true sleep 3 # Start scenario test and keep logs - NNODES=$N_NODE go test -v -run ^TestScenario$ github.com/dedis/d-voting/integration -count=1 | tee ./log/log/gotest.log + NNODES=$N_NODE go test -v -run ^TestScenario$ github.com/c4dt/d-voting/integration -count=1 | tee ./log/log/gotest.log sleep 3 # Stop the test ./kill_test.sh diff --git a/cli/cosipbftcontroller/action_test.go b/cli/cosipbftcontroller/action_test.go index 322b89863..20f9b475f 100644 --- a/cli/cosipbftcontroller/action_test.go +++ b/cli/cosipbftcontroller/action_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/dedis/d-voting/internal/testing/fake" + "github.com/c4dt/d-voting/internal/testing/fake" "github.com/stretchr/testify/require" "go.dedis.ch/dela/cli/node" "go.dedis.ch/dela/core/access" @@ -94,7 +94,6 @@ func TestRosterAddAction_Execute(t *testing.T) { var p pool.Pool require.NoError(t, ctx.Injector.Resolve(&p)) - require.Equal(t, 1, p.Len()) ctx.Injector = node.NewInjector() err = action.Execute(ctx) diff --git a/cli/cosipbftcontroller/mod.go b/cli/cosipbftcontroller/mod.go index 08ae6d413..1b09ac720 100644 --- a/cli/cosipbftcontroller/mod.go +++ b/cli/cosipbftcontroller/mod.go @@ -40,9 +40,6 @@ const ( errInjector = "injector: %v" ) -// valueAccessKey is the access key used for the value contract. -var valueAccessKey = [32]byte{2} - func blsSigner() encoding.BinaryMarshaler { return bls.NewSigner() } @@ -130,7 +127,7 @@ func (m miniController) OnStart(flags cli.Flags, inj node.Injector) error { rosterFac := authority.NewFactory(onet.GetAddressFactory(), cosi.GetPublicKeyFactory()) cosipbft.RegisterRosterContract(exec, rosterFac, access) - value.RegisterContract(exec, value.NewContract(valueAccessKey[:], access)) + value.RegisterContract(exec, value.NewContract(access)) txFac := signed.NewTransactionFactory() vs := simple.NewService(exec, txFac) diff --git a/cli/cosipbftcontroller/mod_test.go b/cli/cosipbftcontroller/mod_test.go index ac9e3525e..5f29fedb1 100644 --- a/cli/cosipbftcontroller/mod_test.go +++ b/cli/cosipbftcontroller/mod_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" - "github.com/dedis/d-voting/services/dkg" + "github.com/c4dt/d-voting/services/dkg" "go.dedis.ch/dela/core/ordering" - "github.com/dedis/d-voting/internal/testing/fake" + "github.com/c4dt/d-voting/internal/testing/fake" "github.com/stretchr/testify/require" "go.dedis.ch/dela/cli" "go.dedis.ch/dela/cli/node" diff --git a/cli/dvoting/mod.go b/cli/dvoting/mod.go index 79428ad64..a378a12c5 100644 --- a/cli/dvoting/mod.go +++ b/cli/dvoting/mod.go @@ -2,28 +2,27 @@ // // Unix example: // -// # Expect GOPATH to be correctly set to have dvoting available. -// go install +// # Expect GOPATH to be correctly set to have dvoting available. +// go install // -// dvoting --config /tmp/node1 start --port 2001 & -// dvoting --config /tmp/node2 start --port 2002 & -// dvoting --config /tmp/node3 start --port 2003 & +// dvoting --config /tmp/node1 start --port 2001 & +// dvoting --config /tmp/node2 start --port 2002 & +// dvoting --config /tmp/node3 start --port 2003 & // -// # Share the different certificates among the participants. -// dvoting --config /tmp/node2 minogrpc join --address 127.0.0.1:2001\ -// $(dvoting --config /tmp/node1 minogrpc token) -// dvoting --config /tmp/node3 minogrpc join --address 127.0.0.1:2001\ -// $(dvoting --config /tmp/node1 minogrpc token) +// # Share the different certificates among the participants. +// dvoting --config /tmp/node2 minogrpc join --address 127.0.0.1:2001\ +// $(dvoting --config /tmp/node1 minogrpc token) +// dvoting --config /tmp/node3 minogrpc join --address 127.0.0.1:2001\ +// $(dvoting --config /tmp/node1 minogrpc token) // -// # Create a chain with two members. -// dvoting --config /tmp/node1 ordering setup\ -// --member $(dvoting --config /tmp/node1 ordering export)\ -// --member $(dvoting --config /tmp/node2 ordering export) -// -// # Add the third after the chain is set up. -// dvoting --config /tmp/node1 ordering roster add\ -// --member $(dvoting --config /tmp/node3 ordering export) +// # Create a chain with two members. +// dvoting --config /tmp/node1 ordering setup\ +// --member $(dvoting --config /tmp/node1 ordering export)\ +// --member $(dvoting --config /tmp/node2 ordering export) // +// # Add the third after the chain is set up. +// dvoting --config /tmp/node1 ordering roster add\ +// --member $(dvoting --config /tmp/node3 ordering export) package main import ( @@ -31,14 +30,14 @@ import ( "io" "os" - dkg "github.com/dedis/d-voting/services/dkg/pedersen/controller" - "github.com/dedis/d-voting/services/dkg/pedersen/json" - shuffle "github.com/dedis/d-voting/services/shuffle/neff/controller" + dkg "github.com/c4dt/d-voting/services/dkg/pedersen/controller" + "github.com/c4dt/d-voting/services/dkg/pedersen/json" + shuffle "github.com/c4dt/d-voting/services/shuffle/neff/controller" - cosipbft "github.com/dedis/d-voting/cli/cosipbftcontroller" - "github.com/dedis/d-voting/cli/postinstall" - evoting "github.com/dedis/d-voting/contracts/evoting/controller" - metrics "github.com/dedis/d-voting/metrics/controller" + cosipbft "github.com/c4dt/d-voting/cli/cosipbftcontroller" + "github.com/c4dt/d-voting/cli/postinstall" + evoting "github.com/c4dt/d-voting/contracts/evoting/controller" + metrics "github.com/c4dt/d-voting/metrics/controller" "go.dedis.ch/dela/cli/node" access "go.dedis.ch/dela/contracts/access/controller" db "go.dedis.ch/dela/core/store/kv/controller" @@ -47,7 +46,7 @@ import ( mino "go.dedis.ch/dela/mino/minogrpc/controller" proxy "go.dedis.ch/dela/mino/proxy/http/controller" - _ "github.com/dedis/d-voting/services/shuffle/neff/json" + _ "github.com/c4dt/d-voting/services/shuffle/neff/json" gapi "go.dedis.ch/dela-apps/gapi/controller" ) diff --git a/cli/dvoting/mod_test.go b/cli/dvoting/mod_test.go index 5c1f8c2bf..08919672f 100644 --- a/cli/dvoting/mod_test.go +++ b/cli/dvoting/mod_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/base64" "fmt" "io" "net" @@ -14,6 +15,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.dedis.ch/kyber/v3/pairing/bn256" ) func TestDvoting_Main(t *testing.T) { @@ -61,7 +63,7 @@ func TestDvoting_Scenario_SetupAndTransactions(t *testing.T) { shareCert(t, node3, node1, "//127.0.0.1:2111") shareCert(t, node5, node1, "//127.0.0.1:2111") - // Setup the chain with nodes 1 and 2. + // Set up the chain with nodes 1 and 2. args := append(append( append( []string{os.Args[0], "--config", node1, "ordering", "setup"}, @@ -85,7 +87,26 @@ func TestDvoting_Scenario_SetupAndTransactions(t *testing.T) { err = run(args) require.NoError(t, err) + // Add the certificate and push two new blocks to make sure node4 is + // fully participating + shareCert(t, node4, node1, "//127.0.0.1:2111") + publicKey, err := bn256.NewSuiteG2().Point().MarshalBinary() + require.NoError(t, err) + publicKeyHex := base64.StdEncoding.EncodeToString(publicKey) + argsAccess := []string{ + os.Args[0], + "--config", node1, "access", "add", + "--identity", publicKeyHex, + } + for i := 0; i < 2; i++ { + err = runWithCfg(argsAccess, config{}) + require.NoError(t, err) + } + // Add node 5 which should be participating. + // This makes sure that node 4 is actually participating and caught up. + // If node 4 is not participating, there would be too many faulty nodes + // after adding node 5. args = append([]string{ os.Args[0], "--config", node1, "ordering", "roster", "add", @@ -96,10 +117,10 @@ func TestDvoting_Scenario_SetupAndTransactions(t *testing.T) { err = run(args) require.NoError(t, err) - // Run a few transactions. - for i := 0; i < 5; i++ { - err = runWithCfg(args, config{}) - require.EqualError(t, err, "command error: transaction refused: duplicate in roster: 127.0.0.1:2115") + // Run 2 new transactions + for i := 0; i < 2; i++ { + err = runWithCfg(argsAccess, config{}) + require.NoError(t, err) } // Test a timeout waiting for a transaction. @@ -155,7 +176,7 @@ func TestDvoting_Scenario_RestartNode(t *testing.T) { ) err = run(args) - require.EqualError(t, err, "command error: transaction refused: duplicate in roster: 127.0.0.1:2210") + require.EqualError(t, err, "command error: transaction refused: duplicate in roster: grpcs://127.0.0.1:2210") } // ----------------------------------------------------------------------------- diff --git a/cli/postinstall/mod.go b/cli/postinstall/mod.go index 560e57d40..1c3e8ccf3 100644 --- a/cli/postinstall/mod.go +++ b/cli/postinstall/mod.go @@ -5,10 +5,10 @@ import ( "path/filepath" "time" - evoting "github.com/dedis/d-voting/contracts/evoting/controller" - prom "github.com/dedis/d-voting/metrics/controller" - dkg "github.com/dedis/d-voting/services/dkg/pedersen/controller" - neff "github.com/dedis/d-voting/services/shuffle/neff/controller" + evoting "github.com/c4dt/d-voting/contracts/evoting/controller" + prom "github.com/c4dt/d-voting/metrics/controller" + dkg "github.com/c4dt/d-voting/services/dkg/pedersen/controller" + neff "github.com/c4dt/d-voting/services/shuffle/neff/controller" "go.dedis.ch/dela" "go.dedis.ch/dela/cli" "go.dedis.ch/dela/cli/node" diff --git a/contracts/evoting/controller/action.go b/contracts/evoting/controller/action.go index 71e15a659..b2d04d026 100644 --- a/contracts/evoting/controller/action.go +++ b/contracts/evoting/controller/action.go @@ -17,13 +17,13 @@ import ( "go.dedis.ch/kyber/v3/sign/schnorr" "go.dedis.ch/kyber/v3/suites" - "github.com/dedis/d-voting/contracts/evoting/types" - "github.com/dedis/d-voting/internal/testing/fake" - eproxy "github.com/dedis/d-voting/proxy" - "github.com/dedis/d-voting/proxy/txnmanager" - ptypes "github.com/dedis/d-voting/proxy/types" - "github.com/dedis/d-voting/services/dkg" - "github.com/dedis/d-voting/services/shuffle" + "github.com/c4dt/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/internal/testing/fake" + eproxy "github.com/c4dt/d-voting/proxy" + "github.com/c4dt/d-voting/proxy/txnmanager" + ptypes "github.com/c4dt/d-voting/proxy/types" + "github.com/c4dt/d-voting/services/dkg" + "github.com/c4dt/d-voting/services/shuffle" "github.com/gorilla/mux" "go.dedis.ch/dela" "go.dedis.ch/dela/cli/node" @@ -45,8 +45,8 @@ import ( ) const ( - contentType = "application/json" - formPath = "/evoting/forms" + contentType = "application/json" + formPath = "/evoting/forms" // FormPathSlash is the path to the form with a trailing slash FormPathSlash = formPath + "/" formIDPath = FormPathSlash + "{formID}" @@ -425,7 +425,8 @@ func (a *scenarioTestAction) Execute(ctx node.Context) error { return xerrors.Errorf(getFormErr, err) } - encryptedBallots := form.Suffragia.Ciphervotes + suff, err := form.Suffragia(serdecontext, service.GetStore()) + encryptedBallots := suff.Ciphervotes dela.Logger.Info().Msg("Length encrypted ballots: " + strconv.Itoa(len(encryptedBallots))) dela.Logger.Info().Msgf("Ballot of user1: %s", encryptedBallots[0]) dela.Logger.Info().Msgf("Ballot of user2: %s", encryptedBallots[1]) @@ -445,7 +446,7 @@ func (a *scenarioTestAction) Execute(ctx node.Context) error { return xerrors.Errorf(getFormErr, err) } - dela.Logger.Info().Msg("Title of the form: " + form.Configuration.MainTitle) + dela.Logger.Info().Msg("Title of the form: " + form.Configuration.Title.En) dela.Logger.Info().Msg("Status of the form: " + strconv.Itoa(int(form.Status))) // ###################################### SHUFFLE BALLOTS ################## @@ -485,7 +486,8 @@ func (a *scenarioTestAction) Execute(ctx node.Context) error { logFormStatus(form) dela.Logger.Info().Msg("Number of shuffled ballots : " + strconv.Itoa(len(form.ShuffleInstances))) - dela.Logger.Info().Msg("Number of encrypted ballots : " + strconv.Itoa(len(form.Suffragia.Ciphervotes))) + suff, err = form.Suffragia(serdecontext, service.GetStore()) + dela.Logger.Info().Msg("Number of encrypted ballots : " + strconv.Itoa(len(suff.Ciphervotes))) // ###################################### REQUEST PUBLIC SHARES ############ @@ -642,7 +644,7 @@ func setupSimpleForm(ctx node.Context, secret kyber.Scalar, proxyAddr1 string, return "", types.Form{}, nil, xerrors.Errorf("formID mismatch: %s != %s", form.FormID, formID) } - fmt.Fprintf(ctx.Out, "Title of the form: "+form.Configuration.MainTitle) + fmt.Fprintf(ctx.Out, "Title of the form: "+form.Configuration.Title.En) fmt.Fprintf(ctx.Out, "ID of the form: "+form.FormID) fmt.Fprintf(ctx.Out, "Status of the form: "+strconv.Itoa(int(form.Status))) @@ -650,7 +652,7 @@ func setupSimpleForm(ctx node.Context, secret kyber.Scalar, proxyAddr1 string, } func logFormStatus(form types.Form) { - dela.Logger.Info().Msg("Title of the form : " + form.Configuration.MainTitle) + dela.Logger.Info().Msg("Title of the form : " + form.Configuration.Title.En) dela.Logger.Info().Msg("ID of the form : " + form.FormID) dela.Logger.Info().Msg("Status of the form : " + strconv.Itoa(int(form.Status))) } diff --git a/contracts/evoting/evoting.go b/contracts/evoting/evoting.go index 0832a3de0..7d3fd1d7b 100644 --- a/contracts/evoting/evoting.go +++ b/contracts/evoting/evoting.go @@ -12,10 +12,10 @@ import ( "strings" "go.dedis.ch/dela" - + "go.dedis.ch/dela/core/ordering/cosipbft/contracts/viewchange" "go.dedis.ch/kyber/v3/share" - "github.com/dedis/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/contracts/evoting/types" "go.dedis.ch/dela/core/execution" "go.dedis.ch/dela/core/execution/native" "go.dedis.ch/dela/core/ordering/cosipbft/authority" @@ -60,7 +60,7 @@ func (e evotingCommand) createForm(snap store.Snapshot, step execution.Step) err return xerrors.Errorf(errWrongTx, msg) } - rosterBuf, err := snap.Get(e.rosterKey) + rosterBuf, err := snap.Get(viewchange.GetRosterKey()) if err != nil { return xerrors.Errorf("failed to get roster") } @@ -91,7 +91,6 @@ func (e evotingCommand) createForm(snap store.Snapshot, step execution.Step) err Status: types.Initial, // Pubkey is set by the opening command BallotSize: tx.Configuration.MaxBallotSize(), - Suffragia: types.Suffragia{}, PubsharesUnits: units, ShuffleInstances: []types.ShuffleInstance{}, DecryptedBallots: []types.Ballot{}, @@ -131,7 +130,10 @@ func (e evotingCommand) createForm(snap store.Snapshot, step execution.Step) err } } - formsMetadata.FormsIDs.Add(form.FormID) + err = formsMetadata.FormsIDs.Add(form.FormID) + if err != nil { + return xerrors.Errorf("couldn't add new form: %v", err) + } formMetadataJSON, err := json.Marshal(formsMetadata) if err != nil { @@ -228,7 +230,10 @@ func (e evotingCommand) castVote(snap store.Snapshot, step execution.Step) error len(tx.Ballot), form.ChunksPerBallot()) } - form.Suffragia.CastVote(tx.UserID, tx.Ballot) + err = form.CastVote(e.context, snap, tx.UserID, tx.Ballot) + if err != nil { + return xerrors.Errorf("couldn't cast vote: %v", err) + } formBuf, err := form.Serialize(e.context) if err != nil { @@ -240,7 +245,7 @@ func (e evotingCommand) castVote(snap store.Snapshot, step execution.Step) error return xerrors.Errorf("failed to set value: %v", err) } - PromFormBallots.WithLabelValues(form.FormID).Set(float64(len(form.Suffragia.Ciphervotes))) + PromFormBallots.WithLabelValues(form.FormID).Set(float64(form.BallotCount)) return nil } @@ -269,7 +274,8 @@ func (e evotingCommand) shuffleBallots(snap store.Snapshot, step execution.Step) } if form.Status != types.Closed { - return xerrors.Errorf("the form is not closed") + return xerrors.Errorf("the form is not in state closed (current: %d != closed: %d)", + form.Status, types.Closed) } // Round starts at 0 @@ -358,7 +364,11 @@ func (e evotingCommand) shuffleBallots(snap store.Snapshot, step execution.Step) var ciphervotes []types.Ciphervote if tx.Round == 0 { - ciphervotes = form.Suffragia.Ciphervotes + suff, err := form.Suffragia(e.context, snap) + if err != nil { + return xerrors.Errorf("couldn't get ballots: %v", err) + } + ciphervotes = suff.Ciphervotes } else { // get the form's last shuffled ballots lastIndex := len(form.ShuffleInstances) - 1 @@ -466,7 +476,7 @@ func (e evotingCommand) closeForm(snap store.Snapshot, step execution.Step) erro return xerrors.Errorf("the form is not open, current status: %d", form.Status) } - if len(form.Suffragia.Ciphervotes) <= 1 { + if form.BallotCount <= 1 { return xerrors.Errorf("at least two ballots are required") } diff --git a/contracts/evoting/json/ciphervote.go b/contracts/evoting/json/ciphervote.go index b8c51b65d..2c9032b5c 100644 --- a/contracts/evoting/json/ciphervote.go +++ b/contracts/evoting/json/ciphervote.go @@ -1,7 +1,7 @@ package json import ( - "github.com/dedis/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/contracts/evoting/types" "go.dedis.ch/dela/serde" "golang.org/x/xerrors" ) diff --git a/contracts/evoting/json/forms.go b/contracts/evoting/json/forms.go index 8497d4ab5..f2aba4031 100644 --- a/contracts/evoting/json/forms.go +++ b/contracts/evoting/json/forms.go @@ -1,9 +1,10 @@ package json import ( + "encoding/hex" "encoding/json" - "github.com/dedis/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/contracts/evoting/types" "go.dedis.ch/dela/core/ordering/cosipbft/authority" ctypes "go.dedis.ch/dela/core/ordering/cosipbft/types" "go.dedis.ch/dela/serde" @@ -35,9 +36,14 @@ func (formFormat) Encode(ctx serde.Context, message serde.Message) ([]byte, erro } } - suffragia, err := encodeSuffragia(ctx, m.Suffragia) - if err != nil { - return nil, xerrors.Errorf("failed to encode suffragia: %v", err) + suffragias := make([]string, len(m.SuffragiaIDs)) + for i, suf := range m.SuffragiaIDs { + suffragias[i] = hex.EncodeToString(suf) + } + + suffragiaHashes := make([]string, len(m.SuffragiaHashes)) + for i, sufH := range m.SuffragiaHashes { + suffragiaHashes[i] = hex.EncodeToString(sufH) } shuffleInstances, err := encodeShuffleInstances(ctx, m.ShuffleInstances) @@ -58,11 +64,13 @@ func (formFormat) Encode(ctx serde.Context, message serde.Message) ([]byte, erro formJSON := FormJSON{ Configuration: m.Configuration, - FormID: m.FormID, + FormID: m.FormID, Status: uint16(m.Status), Pubkey: pubkey, BallotSize: m.BallotSize, - Suffragia: suffragia, + Suffragias: suffragias, + SuffragiaHashes: suffragiaHashes, + BallotCount: m.BallotCount, ShuffleInstances: shuffleInstances, ShuffleThreshold: m.ShuffleThreshold, PubsharesUnits: pubsharesUnits, @@ -100,9 +108,20 @@ func (formFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) } } - suffragia, err := decodeSuffragia(ctx, formJSON.Suffragia) - if err != nil { - return nil, xerrors.Errorf("failed to decode suffragia: %v", err) + suffragias := make([][]byte, len(formJSON.Suffragias)) + for i, suff := range formJSON.Suffragias { + suffragias[i], err = hex.DecodeString(suff) + if err != nil { + return nil, xerrors.Errorf("failed to decode suffragia-address: %v", err) + } + } + + suffragiaHashes := make([][]byte, len(formJSON.SuffragiaHashes)) + for i, suffH := range formJSON.SuffragiaHashes { + suffragiaHashes[i], err = hex.DecodeString(suffH) + if err != nil { + return nil, xerrors.Errorf("failed to decode suffragia-hash: %v", err) + } } shuffleInstances, err := decodeShuffleInstances(ctx, formJSON.ShuffleInstances) @@ -128,11 +147,13 @@ func (formFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) return types.Form{ Configuration: formJSON.Configuration, - FormID: formJSON.FormID, + FormID: formJSON.FormID, Status: types.Status(formJSON.Status), Pubkey: pubKey, BallotSize: formJSON.BallotSize, - Suffragia: suffragia, + SuffragiaIDs: suffragias, + SuffragiaHashes: suffragiaHashes, + BallotCount: formJSON.BallotCount, ShuffleInstances: shuffleInstances, ShuffleThreshold: formJSON.ShuffleThreshold, PubsharesUnits: pubSharesSubmissions, @@ -157,7 +178,14 @@ type FormJSON struct { // to pad smaller ballots such that all ballots cast have the same size BallotSize int - Suffragia SuffragiaJSON + // Suffragias are the hex-encoded addresses of the Suffragia storages. + Suffragias []string + + // BallotCount represents the total number of ballots cast. + BallotCount uint32 + + // SuffragiaHashes are the hex-encoded sha256-hashes of the ballots in every Suffragia. + SuffragiaHashes []string // ShuffleInstances is all the shuffles, along with their proof and identity // of shuffler. @@ -179,62 +207,6 @@ type FormJSON struct { RosterBuf []byte } -// SuffragiaJSON defines the JSON representation of a suffragia. -type SuffragiaJSON struct { - UserIDs []string - Ciphervotes []json.RawMessage -} - -func encodeSuffragia(ctx serde.Context, suffragia types.Suffragia) (SuffragiaJSON, error) { - ciphervotes := make([]json.RawMessage, len(suffragia.Ciphervotes)) - - for i, ciphervote := range suffragia.Ciphervotes { - buff, err := ciphervote.Serialize(ctx) - if err != nil { - return SuffragiaJSON{}, xerrors.Errorf("failed to serialize ciphervote: %v", err) - } - - ciphervotes[i] = buff - } - return SuffragiaJSON{ - UserIDs: suffragia.UserIDs, - Ciphervotes: ciphervotes, - }, nil -} - -func decodeSuffragia(ctx serde.Context, suffragiaJSON SuffragiaJSON) (types.Suffragia, error) { - var res types.Suffragia - fac := ctx.GetFactory(types.CiphervoteKey{}) - - factory, ok := fac.(types.CiphervoteFactory) - if !ok { - return res, xerrors.Errorf("invalid ciphervote factory: '%T'", fac) - } - - ciphervotes := make([]types.Ciphervote, len(suffragiaJSON.Ciphervotes)) - - for i, ciphervoteJSON := range suffragiaJSON.Ciphervotes { - msg, err := factory.Deserialize(ctx, ciphervoteJSON) - if err != nil { - return res, xerrors.Errorf("failed to deserialize ciphervote json: %v", err) - } - - ciphervote, ok := msg.(types.Ciphervote) - if !ok { - return res, xerrors.Errorf("wrong type: '%T'", msg) - } - - ciphervotes[i] = ciphervote - } - - res = types.Suffragia{ - UserIDs: suffragiaJSON.UserIDs, - Ciphervotes: ciphervotes, - } - - return res, nil -} - // ShuffleInstanceJSON defines the JSON representation of a shuffle instance type ShuffleInstanceJSON struct { // ShuffledBallots contains the list of shuffled ciphertext for this round diff --git a/contracts/evoting/json/mod.go b/contracts/evoting/json/mod.go index ab07ec828..c9845338a 100644 --- a/contracts/evoting/json/mod.go +++ b/contracts/evoting/json/mod.go @@ -1,7 +1,7 @@ package json import ( - "github.com/dedis/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/contracts/evoting/types" "go.dedis.ch/dela/serde" ) @@ -9,6 +9,7 @@ import ( func init() { types.RegisterFormFormat(serde.FormatJSON, formFormat{}) + types.RegisterSuffragiaFormat(serde.FormatJSON, suffragiaFormat{}) types.RegisterCiphervoteFormat(serde.FormatJSON, ciphervoteFormat{}) types.RegisterTransactionFormat(serde.FormatJSON, transactionFormat{}) } diff --git a/contracts/evoting/json/suffragia.go b/contracts/evoting/json/suffragia.go new file mode 100644 index 000000000..424c83910 --- /dev/null +++ b/contracts/evoting/json/suffragia.go @@ -0,0 +1,97 @@ +package json + +import ( + "encoding/json" + + "github.com/c4dt/d-voting/contracts/evoting/types" + "go.dedis.ch/dela/serde" + "golang.org/x/xerrors" +) + +type suffragiaFormat struct{} + +func (suffragiaFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, error) { + switch m := msg.(type) { + case types.Suffragia: + sJson, err := encodeSuffragia(ctx, m) + if err != nil { + return nil, xerrors.Errorf("couldn't encode suffragia: %v", err) + } + + buff, err := ctx.Marshal(&sJson) + if err != nil { + return nil, xerrors.Errorf("failed to marshal form: %v", err) + } + + return buff, nil + default: + return nil, xerrors.Errorf("Unknown format: %T", msg) + } +} + +func (suffragiaFormat) Decode(ctx serde.Context, data []byte) (serde.Message, error) { + var sJson SuffragiaJSON + + err := ctx.Unmarshal(data, &sJson) + if err != nil { + return nil, xerrors.Errorf("failed to unmarshal form: %v", err) + } + + return decodeSuffragia(ctx, sJson) +} + +// SuffragiaJSON defines the JSON representation of a suffragia. +type SuffragiaJSON struct { + UserIDs []string + Ciphervotes []json.RawMessage +} + +func encodeSuffragia(ctx serde.Context, suffragia types.Suffragia) (SuffragiaJSON, error) { + ciphervotes := make([]json.RawMessage, len(suffragia.Ciphervotes)) + + for i, ciphervote := range suffragia.Ciphervotes { + buff, err := ciphervote.Serialize(ctx) + if err != nil { + return SuffragiaJSON{}, xerrors.Errorf("failed to serialize ciphervote: %v", err) + } + + ciphervotes[i] = buff + } + return SuffragiaJSON{ + UserIDs: suffragia.UserIDs, + Ciphervotes: ciphervotes, + }, nil +} + +func decodeSuffragia(ctx serde.Context, suffragiaJSON SuffragiaJSON) (types.Suffragia, error) { + var res types.Suffragia + fac := ctx.GetFactory(types.CiphervoteKey{}) + + factory, ok := fac.(types.CiphervoteFactory) + if !ok { + return res, xerrors.Errorf("invalid ciphervote factory: '%T'", fac) + } + + ciphervotes := make([]types.Ciphervote, len(suffragiaJSON.Ciphervotes)) + + for i, ciphervoteJSON := range suffragiaJSON.Ciphervotes { + msg, err := factory.Deserialize(ctx, ciphervoteJSON) + if err != nil { + return res, xerrors.Errorf("failed to deserialize ciphervote json: %v", err) + } + + ciphervote, ok := msg.(types.Ciphervote) + if !ok { + return res, xerrors.Errorf("wrong type: '%T'", msg) + } + + ciphervotes[i] = ciphervote + } + + res = types.Suffragia{ + UserIDs: suffragiaJSON.UserIDs, + Ciphervotes: ciphervotes, + } + + return res, nil +} diff --git a/contracts/evoting/json/transaction.go b/contracts/evoting/json/transaction.go index 106ada9c7..1093416df 100644 --- a/contracts/evoting/json/transaction.go +++ b/contracts/evoting/json/transaction.go @@ -3,7 +3,7 @@ package json import ( "encoding/json" - "github.com/dedis/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/contracts/evoting/types" "go.dedis.ch/dela/serde" "golang.org/x/xerrors" ) @@ -38,7 +38,7 @@ func (transactionFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, e } cv := CastVoteJSON{ - FormID: t.FormID, + FormID: t.FormID, UserID: t.UserID, Ciphervote: ballot, } @@ -47,7 +47,7 @@ func (transactionFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, e case types.CloseForm: ce := CloseFormJSON{ FormID: t.FormID, - UserID: t.UserID, + UserID: t.UserID, } m = TransactionJSON{CloseForm: &ce} @@ -64,7 +64,7 @@ func (transactionFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, e } sb := ShuffleBallotsJSON{ - FormID: t.FormID, + FormID: t.FormID, Round: t.Round, Ciphervotes: ciphervotes, RandomVector: t.RandomVector, @@ -90,25 +90,25 @@ func (transactionFormat) Encode(ctx serde.Context, msg serde.Message) ([]byte, e } rp := RegisterPubSharesJSON{ - FormID: t.FormID, - Index: t.Index, - PubShares: pubShares, - Signature: t.Signature, - PublicKey: t.PublicKey, + FormID: t.FormID, + Index: t.Index, + PubShares: pubShares, + Signature: t.Signature, + PublicKey: t.PublicKey, } m = TransactionJSON{RegisterPubShares: &rp} case types.CombineShares: db := CombineSharesJSON{ FormID: t.FormID, - UserID: t.UserID, + UserID: t.UserID, } m = TransactionJSON{CombineShares: &db} case types.CancelForm: ce := CancelFormJSON{ FormID: t.FormID, - UserID: t.UserID, + UserID: t.UserID, } m = TransactionJSON{CancelForm: &ce} @@ -159,7 +159,7 @@ func (transactionFormat) Decode(ctx serde.Context, data []byte) (serde.Message, case m.CloseForm != nil: return types.CloseForm{ FormID: m.CloseForm.FormID, - UserID: m.CloseForm.UserID, + UserID: m.CloseForm.UserID, }, nil case m.ShuffleBallots != nil: msg, err := decodeShuffleBallots(ctx, *m.ShuffleBallots) @@ -178,12 +178,12 @@ func (transactionFormat) Decode(ctx serde.Context, data []byte) (serde.Message, case m.CombineShares != nil: return types.CombineShares{ FormID: m.CombineShares.FormID, - UserID: m.CombineShares.UserID, + UserID: m.CombineShares.UserID, }, nil case m.CancelForm != nil: return types.CancelForm{ FormID: m.CancelForm.FormID, - UserID: m.CancelForm.UserID, + UserID: m.CancelForm.UserID, }, nil case m.DeleteForm != nil: return types.DeleteForm{ @@ -197,15 +197,15 @@ func (transactionFormat) Decode(ctx serde.Context, data []byte) (serde.Message, // TransactionJSON is the JSON message that wraps the different kinds of // transactions. type TransactionJSON struct { - CreateForm *CreateFormJSON `json:",omitempty"` - OpenForm *OpenFormJSON `json:",omitempty"` + CreateForm *CreateFormJSON `json:",omitempty"` + OpenForm *OpenFormJSON `json:",omitempty"` CastVote *CastVoteJSON `json:",omitempty"` - CloseForm *CloseFormJSON `json:",omitempty"` + CloseForm *CloseFormJSON `json:",omitempty"` ShuffleBallots *ShuffleBallotsJSON `json:",omitempty"` RegisterPubShares *RegisterPubSharesJSON `json:",omitempty"` CombineShares *CombineSharesJSON `json:",omitempty"` - CancelForm *CancelFormJSON `json:",omitempty"` - DeleteForm *DeleteFormJSON `json:",omitempty"` + CancelForm *CancelFormJSON `json:",omitempty"` + DeleteForm *DeleteFormJSON `json:",omitempty"` } // CreateFormJSON is the JSON representation of a CreateForm transaction @@ -221,7 +221,7 @@ type OpenFormJSON struct { // CastVoteJSON is the JSON representation of a CastVote transaction type CastVoteJSON struct { - FormID string + FormID string UserID string Ciphervote json.RawMessage } @@ -229,12 +229,12 @@ type CastVoteJSON struct { // CloseFormJSON is the JSON representation of a CloseForm transaction type CloseFormJSON struct { FormID string - UserID string + UserID string } // ShuffleBallotsJSON is the JSON representation of a ShuffleBallots transaction type ShuffleBallotsJSON struct { - FormID string + FormID string Round int Ciphervotes []json.RawMessage RandomVector types.RandomVector @@ -244,23 +244,23 @@ type ShuffleBallotsJSON struct { } type RegisterPubSharesJSON struct { - FormID string - Index int - PubShares PubsharesUnitJSON - Signature []byte - PublicKey []byte + FormID string + Index int + PubShares PubsharesUnitJSON + Signature []byte + PublicKey []byte } // CombineSharesJSON is the JSON representation of a CombineShares transaction type CombineSharesJSON struct { FormID string - UserID string + UserID string } // CancelFormJSON is the JSON representation of a CancelForm transaction type CancelFormJSON struct { FormID string - UserID string + UserID string } // DeleteFormJSON is the JSON representation of a DeleteForm transaction @@ -286,8 +286,8 @@ func decodeCastVote(ctx serde.Context, m CastVoteJSON) (serde.Message, error) { return types.CastVote{ FormID: m.FormID, - UserID: m.UserID, - Ballot: ciphervote, + UserID: m.UserID, + Ballot: ciphervote, }, nil } @@ -314,7 +314,7 @@ func decodeShuffleBallots(ctx serde.Context, m ShuffleBallotsJSON) (serde.Messag } return types.ShuffleBallots{ - FormID: m.FormID, + FormID: m.FormID, Round: m.Round, ShuffledBallots: ciphervotes, RandomVector: m.RandomVector, @@ -342,10 +342,10 @@ func decodeRegisterPubShares(m RegisterPubSharesJSON) (serde.Message, error) { } return types.RegisterPubShares{ - FormID: m.FormID, - Index: m.Index, - Pubshares: pubShares, - Signature: m.Signature, - PublicKey: m.PublicKey, + FormID: m.FormID, + Index: m.Index, + Pubshares: pubShares, + Signature: m.Signature, + PublicKey: m.PublicKey, }, nil } diff --git a/contracts/evoting/mod.go b/contracts/evoting/mod.go index 7f77b75f3..b431bc445 100644 --- a/contracts/evoting/mod.go +++ b/contracts/evoting/mod.go @@ -1,9 +1,9 @@ package evoting import ( - dvoting "github.com/dedis/d-voting" - "github.com/dedis/d-voting/contracts/evoting/types" - "github.com/dedis/d-voting/services/dkg" + dvoting "github.com/c4dt/d-voting" + "github.com/c4dt/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/services/dkg" "github.com/prometheus/client_golang/prometheus" "go.dedis.ch/dela/core/access" "go.dedis.ch/dela/core/execution" @@ -18,7 +18,7 @@ import ( "golang.org/x/xerrors" // Register the JSON format for the form - _ "github.com/dedis/d-voting/contracts/evoting/json" + _ "github.com/c4dt/d-voting/contracts/evoting/json" ) var ( @@ -67,6 +67,9 @@ const ( var suite = suites.MustFind("Ed25519") const ( + // ContractUID is the UID of the contract + ContractUID = "EVOT" + // ContractName is the name of the contract. ContractName = "go.dedis.ch/dela.Evoting" @@ -126,8 +129,8 @@ const ( // NewCreds creates new credentials for a evoting contract execution. We might // want to use in the future a separate credential for each command. -func NewCreds(id []byte) access.Credential { - return access.NewContractCreds(id, ContractName, credentialAllCommand) +func NewCreds() access.Credential { + return access.NewContractCreds([]byte(ContractUID), ContractName, credentialAllCommand) } // RegisterContract registers the value contract to the given execution service. @@ -143,15 +146,10 @@ type Contract struct { // access is the access control service managing this smart contract access access.Service - // accessKey is the access identifier allowed to use this smart contract - accessKey []byte - cmd commands pedersen dkg.DKG - rosterKey []byte - context serde.Context formFac serde.Factory @@ -160,7 +158,7 @@ type Contract struct { } // NewContract creates a new Value contract -func NewContract(accessKey, rosterKey []byte, srvc access.Service, +func NewContract(srvc access.Service, pedersen dkg.DKG, rosterFac authority.Factory) Contract { ctx := json.NewContext() @@ -170,11 +168,8 @@ func NewContract(accessKey, rosterKey []byte, srvc access.Service, transactionFac := types.NewTransactionFactory(ciphervoteFac) contract := Contract{ - access: srvc, - accessKey: accessKey, - pedersen: pedersen, - - rosterKey: rosterKey, + access: srvc, + pedersen: pedersen, context: ctx, @@ -190,7 +185,7 @@ func NewContract(accessKey, rosterKey []byte, srvc access.Service, // Execute implements native.Contract func (c Contract) Execute(snap store.Snapshot, step execution.Step) error { - creds := NewCreds(c.accessKey) + creds := NewCreds() err := c.access.Match(snap, creds, step.Current.GetIdentity()) if err != nil { @@ -256,6 +251,13 @@ func (c Contract) Execute(snap store.Snapshot, step execution.Step) error { return nil } +// UID returns the unique 4-bytes contract identifier. +// +// - implements native.Contract +func (c Contract) UID() string { + return ContractUID +} + func init() { dvoting.PromCollectors = append(dvoting.PromCollectors, PromFormStatus, diff --git a/contracts/evoting/mod_test.go b/contracts/evoting/mod_test.go index 16e139fd5..435a541d8 100644 --- a/contracts/evoting/mod_test.go +++ b/contracts/evoting/mod_test.go @@ -7,9 +7,9 @@ import ( "strconv" "testing" - "github.com/dedis/d-voting/contracts/evoting/types" - "github.com/dedis/d-voting/internal/testing/fake" - "github.com/dedis/d-voting/services/dkg" + "github.com/c4dt/d-voting/contracts/evoting/types" + "github.com/c4dt/d-voting/internal/testing/fake" + "github.com/c4dt/d-voting/services/dkg" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" "go.dedis.ch/dela/core/access" @@ -63,20 +63,17 @@ func TestExecute(t *testing.T) { actor: fakeDkgActor{}, err: nil, } - var evotingAccessKey = [32]byte{3} - rosterKey := [32]byte{} - service := fakeAccess{err: fake.GetError()} rosterFac := fakeAuthorityFactory{} - contract := NewContract(evotingAccessKey[:], rosterKey[:], service, fakeDkg, rosterFac) + contract := NewContract(service, fakeDkg, rosterFac) err := contract.Execute(fakeStore{}, makeStep(t)) require.EqualError(t, err, "identity not authorized: fake.PublicKey ("+fake.GetError().Error()+")") service = fakeAccess{} - contract = NewContract(evotingAccessKey[:], rosterKey[:], service, fakeDkg, rosterFac) + contract = NewContract(service, fakeDkg, rosterFac) err = contract.Execute(fakeStore{}, makeStep(t)) require.EqualError(t, err, "\"evoting:command\" not found in tx arg") @@ -129,13 +126,10 @@ func TestCommand_CreateForm(t *testing.T) { data, err := createForm.Serialize(ctx) require.NoError(t, err) - var evotingAccessKey = [32]byte{3} - rosterKey := [32]byte{} - service := fakeAccess{err: fake.GetError()} rosterFac := fakeAuthorityFactory{} - contract := NewContract(evotingAccessKey[:], rosterKey[:], service, fakeDkg, rosterFac) + contract := NewContract(service, fakeDkg, rosterFac) cmd := evotingCommand{ Contract: &contract, @@ -301,11 +295,13 @@ func TestCommand_CastVote(t *testing.T) { form, ok := message.(types.Form) require.True(t, ok) - require.Len(t, form.Suffragia.Ciphervotes, 1) - require.True(t, castVote.Ballot.Equal(form.Suffragia.Ciphervotes[0])) + require.Len(t, form.BallotCount, 1) + suff, err := form.Suffragia(ctx, snap) + require.NoError(t, err) + require.True(t, castVote.Ballot.Equal(suff.Ciphervotes[0])) - require.Equal(t, castVote.UserID, form.Suffragia.UserIDs[0]) - require.Equal(t, float64(len(form.Suffragia.Ciphervotes)), testutil.ToFloat64(PromFormBallots)) + require.Equal(t, castVote.UserID, suff.UserIDs[0]) + require.Equal(t, float64(form.BallotCount), testutil.ToFloat64(PromFormBallots)) } func TestCommand_CloseForm(t *testing.T) { @@ -370,8 +366,8 @@ func TestCommand_CloseForm(t *testing.T) { err = cmd.closeForm(snap, makeStep(t, FormArg, string(data))) require.EqualError(t, err, "at least two ballots are required") - dummyForm.Suffragia.CastVote("dummyUser1", types.Ciphervote{}) - dummyForm.Suffragia.CastVote("dummyUser2", types.Ciphervote{}) + require.NoError(t, dummyForm.CastVote(ctx, snap, "dummyUser1", types.Ciphervote{})) + require.NoError(t, dummyForm.CastVote(ctx, snap, "dummyUser2", types.Ciphervote{})) formBuf, err = dummyForm.Serialize(ctx) require.NoError(t, err) @@ -555,7 +551,7 @@ func TestCommand_ShuffleBallotsFormatErrors(t *testing.T) { require.NoError(t, err) err = cmd.shuffleBallots(snap, makeStep(t, FormArg, string(data))) - require.EqualError(t, err, "the form is not closed") + require.EqualError(t, err, "the form is not in state closed (current: 0 != closed: 2)") // Wrong round : form.Status = types.Closed @@ -703,7 +699,6 @@ func TestCommand_ShuffleBallotsFormatErrors(t *testing.T) { form.Pubkey = pubKey shuffleBallots.Round = 0 form.ShuffleInstances = make([]types.ShuffleInstance, 0) - form.Suffragia.Ciphervotes = make([]types.Ciphervote, 0) data, err = shuffleBallots.Serialize(ctx) require.NoError(t, err) @@ -719,9 +714,9 @@ func TestCommand_ShuffleBallotsFormatErrors(t *testing.T) { // > With only one shuffled ballot the shuffling can't happen - form.Suffragia.CastVote("user1", types.Ciphervote{ + require.NoError(t, form.CastVote(ctx, snap, "user1", types.Ciphervote{ types.EGPair{K: suite.Point(), C: suite.Point()}, - }) + })) data, err = shuffleBallots.Serialize(ctx) require.NoError(t, err) @@ -1124,20 +1119,16 @@ func initFormAndContract() (types.Form, Contract) { FormID: fakeFormID, Status: 0, Pubkey: nil, - Suffragia: types.Suffragia{}, ShuffleInstances: make([]types.ShuffleInstance, 0), DecryptedBallots: nil, ShuffleThreshold: 0, Roster: fake.Authority{}, } - var evotingAccessKey = [32]byte{3} - rosterKey := [32]byte{} - service := fakeAccess{err: fake.GetError()} rosterFac := fakeAuthorityFactory{} - contract := NewContract(evotingAccessKey[:], rosterKey[:], service, fakeDkg, rosterFac) + contract := NewContract(service, fakeDkg, rosterFac) return dummyForm, contract } @@ -1165,12 +1156,13 @@ func initGoodShuffleBallot(t *testing.T, k int) (types.Form, types.ShuffleBallot shuffleBallots.Round = 0 form.ShuffleInstances = make([]types.ShuffleInstance, 0) + snap := fake.InMemorySnapshot{} for i := 0; i < k; i++ { ballot := types.Ciphervote{types.EGPair{ K: Ks[i], C: Cs[i], }} - form.Suffragia.CastVote(fmt.Sprintf("user%d", i), ballot) + form.CastVote(ctx, &snap, fmt.Sprintf("user%d", i), ballot) } // Valid Signature of shuffle diff --git a/contracts/evoting/types/ballots.go b/contracts/evoting/types/ballots.go index c6855251e..907678a2f 100644 --- a/contracts/evoting/types/ballots.go +++ b/contracts/evoting/types/ballots.go @@ -41,14 +41,9 @@ type Ballot struct { } // Unmarshal decodes the given string according to the format described in -// "state of smart contract.md" +// "/docs/state_of_smart_contract.md" +// TODO: actually describe the format in there... func (b *Ballot) Unmarshal(marshalledBallot string, form Form) error { - if len(marshalledBallot) > form.BallotSize { - b.invalidate() - return fmt.Errorf("ballot has an unexpected size %d, expected <= %d", - len(marshalledBallot), form.BallotSize) - } - lines := strings.Split(marshalledBallot, "\n") b.SelectResultIDs = make([]ID, 0) @@ -258,12 +253,26 @@ func (b *Ballot) Equal(other Ballot) bool { return true } +// Title contains the titles in different languages. +type Title struct { + En string + Fr string + De string +} + +// Hint contains explanations in different languages. +type Hint struct { + En string + Fr string + De string +} + // Subject is a wrapper around multiple questions that can be of type "select", // "rank", or "text". type Subject struct { ID ID - Title string + Title Title // Order defines the order of the different question, which all have a unique // identifier. This is purely for display purpose. @@ -419,11 +428,11 @@ func isValid(q Question) bool { type Select struct { ID ID - Title string + Title Title MaxN uint MinN uint Choices []string - Hint string + Hint Hint } // GetID implements Question @@ -485,11 +494,11 @@ func (s Select) unmarshalAnswers(sforms []string) ([]bool, error) { type Rank struct { ID ID - Title string + Title Title MaxN uint MinN uint Choices []string - Hint string + Hint Hint } func (r Rank) GetID() string { @@ -558,13 +567,13 @@ func (r Rank) unmarshalAnswers(ranks []string) ([]int8, error) { type Text struct { ID ID - Title string + Title Title MaxN uint MinN uint MaxLength uint Regex string Choices []string - Hint string + Hint Hint } func (t Text) GetID() string { diff --git a/contracts/evoting/types/ballots_test.go b/contracts/evoting/types/ballots_test.go index 47dd9828b..bc94dea27 100644 --- a/contracts/evoting/types/ballots_test.go +++ b/contracts/evoting/types/ballots_test.go @@ -56,13 +56,13 @@ func TestBallot_Unmarshal(t *testing.T) { Selects: []Select{{ ID: decodedQuestionID(1), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 2, MinN: 2, Choices: make([]string, 3), }, { ID: decodedQuestionID(2), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 3, MinN: 3, Choices: make([]string, 5), @@ -70,7 +70,7 @@ func TestBallot_Unmarshal(t *testing.T) { Ranks: []Rank{{ ID: decodedQuestionID(3), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 4, MinN: 0, Choices: make([]string, 4), @@ -78,7 +78,7 @@ func TestBallot_Unmarshal(t *testing.T) { Texts: []Text{{ ID: decodedQuestionID(4), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 2, MinN: 2, MaxLength: 10, @@ -111,10 +111,6 @@ func TestBallot_Unmarshal(t *testing.T) { require.Equal(t, expected.TextResultIDs, b.TextResultIDs) require.Equal(t, expected.TextResult, b.TextResult) - // with ballot too long - err = b.Unmarshal(ballot1+"x", form) - require.EqualError(t, err, "ballot has an unexpected size 102, expected <= 101") - // with line wrongly formatted err = b.Unmarshal("x", form) require.EqualError(t, err, "a line in the ballot has length != 3: x") @@ -309,7 +305,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { subject := Subject{ Subjects: []Subject{{ ID: "", - Title: "", + Title: Title{En: "", Fr: "", De: ""}, Order: nil, Subjects: []Subject{}, Selects: []Select{}, @@ -319,13 +315,13 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Selects: []Select{{ ID: encodedQuestionID(1), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 3, MinN: 0, Choices: make([]string, 3), }, { ID: encodedQuestionID(2), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 5, MinN: 0, Choices: make([]string, 5), @@ -333,7 +329,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Ranks: []Rank{{ ID: encodedQuestionID(3), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 4, MinN: 0, Choices: make([]string, 4), @@ -341,7 +337,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Texts: []Text{{ ID: encodedQuestionID(4), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 2, MinN: 0, MaxLength: 10, @@ -349,7 +345,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Choices: make([]string, 2), }, { ID: encodedQuestionID(5), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 1, MinN: 0, MaxLength: 10, @@ -359,8 +355,8 @@ func TestSubject_MaxEncodedSize(t *testing.T) { } conf := Configuration{ - MainTitle: "", - Scaffold: []Subject{subject}, + Title: Title{En: "", Fr: "", De: ""}, + Scaffold: []Subject{subject}, } size := conf.MaxBallotSize() @@ -372,7 +368,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { func TestSubject_IsValid(t *testing.T) { mainSubject := &Subject{ ID: ID(base64.StdEncoding.EncodeToString([]byte("S1"))), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, Order: []ID{}, Subjects: []Subject{}, Selects: []Select{}, @@ -382,7 +378,7 @@ func TestSubject_IsValid(t *testing.T) { subSubject := &Subject{ ID: ID(base64.StdEncoding.EncodeToString([]byte("S2"))), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, Order: []ID{}, Subjects: []Subject{}, Selects: []Select{}, @@ -391,8 +387,8 @@ func TestSubject_IsValid(t *testing.T) { } configuration := Configuration{ - MainTitle: "", - Scaffold: []Subject{*mainSubject, *subSubject}, + Title: Title{En: "", Fr: "", De: ""}, + Scaffold: []Subject{*mainSubject, *subSubject}, } valid := configuration.IsValid() @@ -404,7 +400,7 @@ func TestSubject_IsValid(t *testing.T) { mainSubject.Selects = []Select{{ ID: encodedQuestionID(1), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 0, MinN: 0, Choices: make([]string, 0), @@ -412,7 +408,7 @@ func TestSubject_IsValid(t *testing.T) { mainSubject.Ranks = []Rank{{ ID: encodedQuestionID(1), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 0, MinN: 0, Choices: make([]string, 0), @@ -427,7 +423,7 @@ func TestSubject_IsValid(t *testing.T) { mainSubject.Ranks[0] = Rank{ ID: encodedQuestionID(2), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 0, MinN: 2, Choices: make([]string, 0), @@ -443,7 +439,7 @@ func TestSubject_IsValid(t *testing.T) { mainSubject.Ranks = []Rank{} mainSubject.Selects[0] = Select{ ID: encodedQuestionID(1), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 1, MinN: 0, Choices: make([]string, 0), @@ -459,7 +455,7 @@ func TestSubject_IsValid(t *testing.T) { mainSubject.Selects = []Select{} mainSubject.Texts = []Text{{ ID: encodedQuestionID(3), - Title: "", + Title: Title{En: "", Fr: "", De: ""}, MaxN: 2, MinN: 4, MaxLength: 0, diff --git a/contracts/evoting/types/election.go b/contracts/evoting/types/election.go index de35d9630..6a566780d 100644 --- a/contracts/evoting/types/election.go +++ b/contracts/evoting/types/election.go @@ -1,10 +1,15 @@ package types import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" "io" "go.dedis.ch/dela/core/ordering/cosipbft/authority" ctypes "go.dedis.ch/dela/core/ordering/cosipbft/types" + "go.dedis.ch/dela/core/store" "go.dedis.ch/dela/serde" "go.dedis.ch/dela/serde/registry" "go.dedis.ch/kyber/v3" @@ -21,7 +26,6 @@ type ID string type Status uint16 const ( - // DecryptedBallots = 4 // Initial is when the form has just been created Initial Status = 0 // Open is when the form is open, i.e. it fetched the public key @@ -38,6 +42,13 @@ const ( Canceled Status = 6 ) +// BallotsPerBlock to improve performance, so that (de)serializing only touches +// 100 ballots at a time. +var BallotsPerBlock = uint32(100) + +// TestCastBallots: if true, automatically fills every block with ballots. +var TestCastBallots = false + // formFormat contains the supported formats for the form. Right now // only JSON is supported. var formFormat = registry.NewSimpleRegistry() @@ -67,8 +78,19 @@ type Form struct { // to pad smaller ballots such that all ballots cast have the same size BallotSize int - // Suffragia is a map from User ID to their encrypted ballot - Suffragia Suffragia + // SuffragiaIDs holds a slice of IDs to slices of SuffragiaIDs. + // This is to optimize the time it takes to (De)serialize a Form. + SuffragiaIDs [][]byte + + // BallotCount is the total number of ballots cast, including double + // ballots. + BallotCount uint32 + + // SuffragiaHashes holds a slice of hashes to all SuffragiaIDs. + // LG: not really sure if this is needed. In case a Form has also to be + // proven to be correct outside the nodes, the hashes are definitely + // needed. + SuffragiaHashes [][]byte // ShuffleInstances is all the shuffles, along with their proof and identity // of shuffler. @@ -146,6 +168,84 @@ func (e *Form) ChunksPerBallot() int { return e.BallotSize/29 + 1 } +// CastVote stores the new vote in the memory. +func (s *Form) CastVote(ctx serde.Context, st store.Snapshot, userID string, ciphervote Ciphervote) error { + var suff Suffragia + var blockID []byte + if s.BallotCount%BallotsPerBlock == 0 { + // Need to create a random ID for storing the ballots. + // H( formID | ballotcount ) + // should be random enough, even if it's previsible. + id, err := hex.DecodeString(s.FormID) + if err != nil { + return xerrors.Errorf("couldn't decode formID: %v", err) + } + h := sha256.New() + h.Write(id) + binary.LittleEndian.PutUint32(id, s.BallotCount) + blockID = h.Sum(id[0:4])[:32] + err = st.Set(blockID, []byte{}) + if err != nil { + return xerrors.Errorf("couldn't store new ballot block: %v", err) + } + s.SuffragiaIDs = append(s.SuffragiaIDs, blockID) + s.SuffragiaHashes = append(s.SuffragiaHashes, []byte{}) + } else { + blockID = s.SuffragiaIDs[len(s.SuffragiaIDs)-1] + buf, err := st.Get(blockID) + if err != nil { + return xerrors.Errorf("couldn't get ballots block: %v", err) + } + format := suffragiaFormat.Get(ctx.GetFormat()) + ctx = serde.WithFactory(ctx, CiphervoteKey{}, CiphervoteFactory{}) + msg, err := format.Decode(ctx, buf) + if err != nil { + return xerrors.Errorf("couldn't unmarshal ballots block in cast: %v", err) + } + suff = msg.(Suffragia) + } + + suff.CastVote(userID, ciphervote) + if TestCastBallots { + for i := uint32(1); i < BallotsPerBlock; i++ { + suff.CastVote(fmt.Sprintf("%s-%d", userID, i), ciphervote) + } + s.BallotCount += BallotsPerBlock - 1 + } + buf, err := suff.Serialize(ctx) + if err != nil { + return xerrors.Errorf("couldn't marshal ballots block: %v", err) + } + err = st.Set(blockID, buf) + s.BallotCount += 1 + return nil +} + +// Suffragia returns all ballots from the storage. This should only +// be called rarely, as it might take a long time. +// It overwrites ballots cast by the same user and keeps only +// the latest ballot. +func (s *Form) Suffragia(ctx serde.Context, rd store.Readable) (Suffragia, error) { + var suff Suffragia + for _, id := range s.SuffragiaIDs { + buf, err := rd.Get(id) + if err != nil { + return suff, xerrors.Errorf("couldn't get ballot block: %v", err) + } + format := suffragiaFormat.Get(ctx.GetFormat()) + ctx = serde.WithFactory(ctx, CiphervoteKey{}, CiphervoteFactory{}) + msg, err := format.Decode(ctx, buf) + if err != nil { + return suff, xerrors.Errorf("couldn't unmarshal ballots block in cast: %v", err) + } + suffTmp := msg.(Suffragia) + for i, uid := range suffTmp.UserIDs { + suff.CastVote(uid, suffTmp.Ciphervotes[i]) + } + } + return suff, nil +} + // RandomVector is a slice of kyber.Scalar (encoded) which is used to prove // and verify the proof of a shuffle type RandomVector [][]byte @@ -197,8 +297,8 @@ type ShuffleInstance struct { // Configuration contains the configuration of a new poll. type Configuration struct { - MainTitle string - Scaffold []Subject + Title Title + Scaffold []Subject } // MaxBallotSize returns the maximum number of bytes required to store a ballot @@ -239,80 +339,6 @@ func (c *Configuration) IsValid() bool { return true } -type Suffragia struct { - UserIDs []string - Ciphervotes []Ciphervote -} - -// CastVote adds a new vote and its associated user or updates a user's vote. -func (s *Suffragia) CastVote(userID string, ciphervote Ciphervote) { - for i, u := range s.UserIDs { - if u == userID { - s.Ciphervotes[i] = ciphervote - return - } - } - - s.UserIDs = append(s.UserIDs, userID) - s.Ciphervotes = append(s.Ciphervotes, ciphervote.Copy()) -} - -// CiphervotesFromPairs transforms two parallel lists of EGPoints to a list of -// Ciphervotes. -func CiphervotesFromPairs(X, Y [][]kyber.Point) ([]Ciphervote, error) { - if len(X) != len(Y) { - return nil, xerrors.Errorf("X and Y must have same length: %d != %d", - len(X), len(Y)) - } - - if len(X) == 0 { - return nil, xerrors.Errorf("ElGamal pairs are empty") - } - - NQ := len(X) // sequence size - k := len(X[0]) // number of votes - res := make([]Ciphervote, k) - - for i := 0; i < k; i++ { - x := make([]kyber.Point, NQ) - y := make([]kyber.Point, NQ) - - for j := 0; j < NQ; j++ { - x[j] = X[j][i] - y[j] = Y[j][i] - } - - ciphervote, err := ciphervoteFromPairs(x, y) - if err != nil { - return nil, xerrors.Errorf("failed to init from ElGamal pairs: %v", err) - } - - res[i] = ciphervote - } - - return res, nil -} - -// ciphervoteFromPairs transforms two parallel lists of EGPoints to a list of -// ElGamal pairs. -func ciphervoteFromPairs(ks []kyber.Point, cs []kyber.Point) (Ciphervote, error) { - if len(ks) != len(cs) { - return Ciphervote{}, xerrors.Errorf("ks and cs must have same length: %d != %d", - len(ks), len(cs)) - } - - res := make(Ciphervote, len(ks)) - - for i := range ks { - res[i] = EGPair{ - K: ks[i], - C: cs[i], - } - } - - return res, nil -} - // Pubshare represents a public share. type Pubshare kyber.Point diff --git a/contracts/evoting/types/suffragia.go b/contracts/evoting/types/suffragia.go new file mode 100644 index 000000000..c8a507440 --- /dev/null +++ b/contracts/evoting/types/suffragia.go @@ -0,0 +1,119 @@ +package types + +import ( + "crypto/sha256" + + "go.dedis.ch/dela/serde" + "go.dedis.ch/dela/serde/registry" + "go.dedis.ch/kyber/v3" + "golang.org/x/xerrors" +) + +// suffragiaFormat contains the supported formats for the form. Right now +// only JSON is supported. +var suffragiaFormat = registry.NewSimpleRegistry() + +// RegisterSuffragiaFormat registers the engine for the provided format +func RegisterSuffragiaFormat(format serde.Format, engine serde.FormatEngine) { + suffragiaFormat.Register(format, engine) +} + +type Suffragia struct { + UserIDs []string + Ciphervotes []Ciphervote +} + +// Serialize implements the serde.Message +func (s Suffragia) Serialize(ctx serde.Context) ([]byte, error) { + format := suffragiaFormat.Get(ctx.GetFormat()) + + data, err := format.Encode(ctx, s) + if err != nil { + return nil, xerrors.Errorf("failed to encode form: %v", err) + } + + return data, nil +} + +// CastVote adds a new vote and its associated user or updates a user's vote. +func (s *Suffragia) CastVote(userID string, ciphervote Ciphervote) { + for i, u := range s.UserIDs { + if u == userID { + s.Ciphervotes[i] = ciphervote + return + } + } + + s.UserIDs = append(s.UserIDs, userID) + s.Ciphervotes = append(s.Ciphervotes, ciphervote.Copy()) +} + +// Hash returns the hash of this list of ballots. +func (s *Suffragia) Hash(ctx serde.Context) ([]byte, error) { + h := sha256.New() + for i, u := range s.UserIDs { + h.Write([]byte(u)) + buf, err := s.Ciphervotes[i].Serialize(ctx) + if err != nil { + return nil, xerrors.Errorf("couldn't serialize ciphervote: %v", err) + } + h.Write(buf) + } + return h.Sum(nil), nil +} + +// CiphervotesFromPairs transforms two parallel lists of EGPoints to a list of +// Ciphervotes. +func CiphervotesFromPairs(X, Y [][]kyber.Point) ([]Ciphervote, error) { + if len(X) != len(Y) { + return nil, xerrors.Errorf("X and Y must have same length: %d != %d", + len(X), len(Y)) + } + + if len(X) == 0 { + return nil, xerrors.Errorf("ElGamal pairs are empty") + } + + NQ := len(X) // sequence size + k := len(X[0]) // number of votes + res := make([]Ciphervote, k) + + for i := 0; i < k; i++ { + x := make([]kyber.Point, NQ) + y := make([]kyber.Point, NQ) + + for j := 0; j < NQ; j++ { + x[j] = X[j][i] + y[j] = Y[j][i] + } + + ciphervote, err := ciphervoteFromPairs(x, y) + if err != nil { + return nil, xerrors.Errorf("failed to init from ElGamal pairs: %v", err) + } + + res[i] = ciphervote + } + + return res, nil +} + +// ciphervoteFromPairs transforms two parallel lists of EGPoints to a list of +// ElGamal pairs. +func ciphervoteFromPairs(ks []kyber.Point, cs []kyber.Point) (Ciphervote, error) { + if len(ks) != len(cs) { + return Ciphervote{}, xerrors.Errorf("ks and cs must have same length: %d != %d", + len(ks), len(cs)) + } + + res := make(Ciphervote, len(ks)) + + for i := range ks { + res[i] = EGPair{ + K: ks[i], + C: cs[i], + } + } + + return res, nil +} diff --git a/contracts/evoting/types/transactions.go b/contracts/evoting/types/transactions.go index b13b03cbe..48ff3733d 100644 --- a/contracts/evoting/types/transactions.go +++ b/contracts/evoting/types/transactions.go @@ -133,8 +133,8 @@ func (oe OpenForm) Serialize(ctx serde.Context) ([]byte, error) { type CastVote struct { // FormID is hex-encoded FormID string - UserID string - Ballot Ciphervote + UserID string + Ballot Ciphervote } // Serialize implements serde.Message @@ -155,7 +155,7 @@ func (cv CastVote) Serialize(ctx serde.Context) ([]byte, error) { type CloseForm struct { // FormID is hex-encoded FormID string - UserID string + UserID string } // Serialize implements serde.Message @@ -175,7 +175,7 @@ func (ce CloseForm) Serialize(ctx serde.Context) ([]byte, error) { // - implements serde.Message // - implements serde.Fingerprinter type ShuffleBallots struct { - FormID string + FormID string Round int ShuffledBallots []Ciphervote // RandomVector is the vector to be used to generate the proof of the next @@ -239,7 +239,7 @@ func (rp RegisterPubShares) Serialize(ctx serde.Context) ([]byte, error) { type CombineShares struct { // FormID is hex-encoded FormID string - UserID string + UserID string } // Serialize implements serde.Message @@ -260,7 +260,7 @@ func (db CombineShares) Serialize(ctx serde.Context) ([]byte, error) { type CancelForm struct { // FormID is hex-encoded FormID string - UserID string + UserID string } // Serialize implements serde.Message diff --git a/deb-package/README.md b/deb-package/README.md index ddcfa9144..99e609bf1 100644 --- a/deb-package/README.md +++ b/deb-package/README.md @@ -171,7 +171,7 @@ PK=<> # taken from the "ordering export", the part after ":" sudo dvoting --config /var/opt/dedis/dvoting/data/dela pool add \ --key $keypath \ --args go.dedis.ch/dela.ContractArg --args go.dedis.ch/dela.Access \ - --args access:grant_id --args 0300000000000000000000000000000000000000000000000000000000000000 \ + --args access:grant_id --args 45564f54 \ --args access:grant_contract --args go.dedis.ch/dela.Evoting \ --args access:grant_command --args all \ --args access:identity --args $PK \ @@ -198,7 +198,7 @@ sudo apt install rubygems build-essential git ## Get the code ```sh -git clone https://github.com/dedis/d-voting.git +git clone https://github.com/c4dt/d-voting.git ``` ## Build the deb package diff --git a/deb-package/build-deb.sh b/deb-package/build-deb.sh index a374e11fa..0a61454ec 100755 --- a/deb-package/build-deb.sh +++ b/deb-package/build-deb.sh @@ -45,7 +45,7 @@ fpm \ --after-install pkg/after-install.sh \ --before-remove pkg/before-remove.sh \ --after-remove pkg/after-remove.sh \ - --url https://dedis.github.com/dedis/dvoting \ + --url https://dedis.github.com/c4dt/dvoting \ --description 'D-Voting package' \ --package dist . diff --git a/docker-compose/docker-compose.debug.yml b/docker-compose/docker-compose.debug.yml index 52078a067..66a1ea7f5 100644 --- a/docker-compose/docker-compose.debug.yml +++ b/docker-compose/docker-compose.debug.yml @@ -1,139 +1,105 @@ +version: "3.8" +x-dela: &dela + build: + dockerfile: Dockerfiles/Dockerfile.dela + context: ../ + target: build + env_file: ../.env + profiles: + - dela + - all +x-dela-env: &dela-env + PROXYKEY: ${PUBLIC_KEY} + PROXY_LOG: info + LLVL: debug + services: dela-worker-0: # inital DELA leader node - image: dela:latest - build: - dockerfile: Dockerfiles/Dockerfile.dela.debug - context: ../ + <<: *dela environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - PROXY_LOG: info - LLVL: debug - NODEPORT: ${NODEPORT} + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-0:2000 volumes: - dela-worker-0-data:/data - hostname: dela-worker-0 ports: - 127.0.0.1:40000:40000 - security_opt: - - apparmor:unconfined - cap_add: - - SYS_PTRACE networks: d-voting: ipv4_address: 172.19.44.254 dela-worker-1: # DELA worker node - image: dela:latest - build: - dockerfile: Dockerfiles/Dockerfile.dela.debug - context: ../ - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - PROXY_LOG: info - LLVL: debug - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-1-data:/data - hostname: dela-worker-1 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-1:2000 ports: - 127.0.0.1:40001:40000 - security_opt: - - apparmor:unconfined - cap_add: - - SYS_PTRACE networks: d-voting: ipv4_address: 172.19.44.253 dela-worker-2: # DELA worker node - image: dela:latest - build: - dockerfile: Dockerfiles/Dockerfile.dela.debug - context: ../ - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - PROXY_LOG: info - LLVL: debug - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-2-data:/data - hostname: dela-worker-2 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-2:2000 ports: - 127.0.0.1:40002:40000 - security_opt: - - apparmor:unconfined - cap_add: - - SYS_PTRACE networks: d-voting: ipv4_address: 172.19.44.252 dela-worker-3: # DELA worker node - image: dela:latest - build: - dockerfile: Dockerfiles/Dockerfile.dela.debug - context: ../ - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - PROXY_LOG: info - LLVL: debug - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-3-data:/data - hostname: dela-worker-3 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-3:2000 ports: - 127.0.0.1:40003:40000 - security_opt: - - apparmor:unconfined - cap_add: - - SYS_PTRACE networks: d-voting: ipv4_address: 172.19.44.251 frontend: # web service frontend - image: frontend:latest + image: ghcr.io/c4dt/d-voting-frontend:latest build: dockerfile: Dockerfiles/Dockerfile.frontend context: ../ ports: - 127.0.0.1:3000:3000 + command: run start volumes: - - ../web/frontend/src:/web/frontend/src - environment: - BACKEND_HOST: ${BACKEND_HOST} - BACKEND_PORT: ${BACKEND_PORT} + - ../web/frontend/src:/web/frontend/src # mount codebase for development + env_file: ../.env + profiles: + - client + - all networks: d-voting: ipv4_address: 172.19.44.2 backend: # web service backend - image: backend:latest + image: ghcr.io/c4dt/d-voting-backend:latest build: dockerfile: Dockerfiles/Dockerfile.backend context: ../ - environment: - DATABASE_USERNAME: ${DATABASE_USERNAME} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - DATABASE_HOST: ${DATABASE_HOST} - DATABASE_PORT: ${DATABASE_PORT} - DB_PATH: /data/${DB_PATH} - FRONT_END_URL: ${FRONT_END_URL} - DELA_NODE_URL: ${DELA_NODE_URL} - SESSION_SECRET: ${SESSION_SECRET} - PUBLIC_KEY: ${PUBLIC_KEY} - PRIVATE_KEY: ${PRIVATE_KEY} + command: run start-dev + env_file: ../.env ports: - 127.0.0.1:5000:5000 - - 127.0.0.1:80:80 depends_on: db: condition: service_started volumes: - backend-data:/data - - ../web/backend/src:/web/backend/src + - ../web/backend/src:/web/backend/src # mount codebase for development + profiles: + - client + - all networks: d-voting: ipv4_address: 172.19.44.3 @@ -146,10 +112,36 @@ services: volumes: - postgres-data:/var/lib/postgresql/data - ../web/backend/src/migration.sql:/docker-entrypoint-initdb.d/init.sql + profiles: + - client + - all networks: d-voting: ipv4_address: 172.19.44.4 + shell: # helper container to execute scripts from within Docker network (macOS/Windows setup) + image: buildpack-deps:bookworm-curl + env_file: ../.env + profiles: + - debug + - all + networks: + d-voting: + ipv4_address: 172.19.44.5 + volumes: + - ../:/src + + firefox: # helper container to execute Firefox within Docker network (macOS/Windows setup) + image: jlesage/firefox + profiles: + - debug + - all + ports: + - 127.0.0.1:5800:5800 + networks: + d-voting: + ipv4_address: 172.19.44.6 + volumes: postgres-data: # PostgreSQL database dela-worker-0-data: diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index ad0a468b1..2e86e7634 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,82 +1,77 @@ +version: "3.8" +x-dela: &dela + image: ghcr.io/c4dt/d-voting-dela:latest + env_file: ../.env +x-dela-env: &dela-env + PROXYKEY: ${PUBLIC_KEY} + PROXY_LOG: info + LLVL: info + services: dela-worker-0: # inital DELA leader node - image: ghcr.io/c4dt/d-voting-dela:latest + <<: *dela + build: + dockerfile: Dockerfiles/Dockerfile.dela + context: ../ + target: build environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - LLVL: info - NODEPORT: ${NODEPORT} + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-0:2000 volumes: - dela-worker-0-data:/data - hostname: dela-worker-0 networks: d-voting: ipv4_address: 172.19.44.254 dela-worker-1: # DELA worker node - image: ghcr.io/c4dt/d-voting-dela:latest - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - LLVL: info - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-1-data:/data - hostname: dela-worker-1 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-1:2000 networks: d-voting: ipv4_address: 172.19.44.253 dela-worker-2: # DELA worker node - image: ghcr.io/c4dt/d-voting-dela:latest - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - LLVL: info - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-2-data:/data - hostname: dela-worker-2 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-2:2000 networks: d-voting: ipv4_address: 172.19.44.252 dela-worker-3: # DELA worker node - image: ghcr.io/c4dt/d-voting-dela:latest - environment: - PROXYKEY: ${PUBLIC_KEY} - PROXYPORT: ${PROXYPORT} - LLVL: info - NODEPORT: ${NODEPORT} + <<: *dela volumes: - dela-worker-3-data:/data - hostname: dela-worker-3 + environment: + <<: *dela-env + PUBLIC_URL: grpc://dela-worker-3:2000 networks: d-voting: ipv4_address: 172.19.44.251 frontend: # web service frontend image: ghcr.io/c4dt/d-voting-frontend:latest + build: + dockerfile: Dockerfiles/Dockerfile.frontend + context: ../ ports: - 127.0.0.1:3000:3000 - environment: - BACKEND_HOST: ${BACKEND_HOST} - BACKEND_PORT: ${BACKEND_PORT} + env_file: ../.env networks: d-voting: ipv4_address: 172.19.44.2 backend: # web service backend image: ghcr.io/c4dt/d-voting-backend:latest - environment: - DATABASE_USERNAME: ${DATABASE_USERNAME} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - DATABASE_HOST: ${DATABASE_HOST} - DATABASE_PORT: ${DATABASE_PORT} - DB_PATH: /data/${DB_PATH} - FRONT_END_URL: ${FRONT_END_URL} - DELA_NODE_URL: ${DELA_NODE_URL} - SESSION_SECRET: ${SESSION_SECRET} - PUBLIC_KEY: ${PUBLIC_KEY} - PRIVATE_KEY: ${PRIVATE_KEY} + build: + dockerfile: Dockerfiles/Dockerfile.backend + context: ../ + env_file: ../.env ports: - 127.0.0.1:5000:5000 depends_on: @@ -95,7 +90,7 @@ services: POSTGRES_PASSWORD: ${DATABASE_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data - - ./web/backend/src/migration.sql:/docker-entrypoint-initdb.d/init.sql + - ../web/backend/src/migration.sql:/docker-entrypoint-initdb.d/init.sql networks: d-voting: ipv4_address: 172.19.44.4 diff --git a/docs/coverpage.md b/docs/coverpage.md index df717a4c8..4462113e3 100644 --- a/docs/coverpage.md +++ b/docs/coverpage.md @@ -5,7 +5,7 @@

- +

- An open platform to run voting instances on a blockchain diff --git a/docs/index.html b/docs/index.html index 1fd7617bc..4a414ee1a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -13,7 +13,7 @@