diff --git a/api/types.go b/api/types.go index 6860af23..72352323 100644 --- a/api/types.go +++ b/api/types.go @@ -16,7 +16,7 @@ type CensusParticipant struct { // Vote is the struct to represent a vote in the system. It will be provided by // the user to cast a vote in a process. type Vote struct { - ProcessID *types.ProcessID `json:"processId"` + ProcessID types.ProcessID `json:"processId"` CensusProof types.CensusProof `json:"censusProof"` Ballot *elgamal.Ballot `json:"ballot"` BallotProof *circomgnark.CircomProof `json:"ballotProof"` diff --git a/api/vote.go b/api/vote.go index ff503263..d727bbee 100644 --- a/api/vote.go +++ b/api/vote.go @@ -117,7 +117,7 @@ func (a *API) newVote(w http.ResponseWriter, r *http.Request) { return } // get the process from the storage - process, err := a.storage.Process(*vote.ProcessID) + process, err := a.storage.Process(vote.ProcessID) if err != nil { ErrResourceNotFound.Withf("could not get process: %v", err).Write(w) return @@ -139,7 +139,7 @@ func (a *API) newVote(w http.ResponseWriter, r *http.Request) { // the vote will be accepted, but it is a precondition to accept the vote, // for example, if the process is not in this sequencer, the vote will be // rejected - if ok, err := a.storage.ProcessIsAcceptingVotes(*vote.ProcessID); !ok { + if ok, err := a.storage.ProcessIsAcceptingVotes(vote.ProcessID); !ok { if err != nil { ErrProcessNotAcceptingVotes.WithErr(err).Write(w) return @@ -156,7 +156,7 @@ func (a *API) newVote(w http.ResponseWriter, r *http.Request) { return } if !isOverwrite { - if maxVotersReached, err := a.storage.ProcessMaxVotersReached(*vote.ProcessID); err != nil { + if maxVotersReached, err := a.storage.ProcessMaxVotersReached(vote.ProcessID); err != nil { ErrGenericInternalServerError.Withf("could not check max voters: %v", err).Write(w) return } else if maxVotersReached { @@ -195,7 +195,7 @@ func (a *API) newVote(w http.ResponseWriter, r *http.Request) { } // calculate the ballot inputs hash ballotInputsHash, err := ballotproof.BallotInputsHash( - *vote.ProcessID, + vote.ProcessID, process.BallotMode, new(bjj.BJJ).SetPoint(process.EncryptionKey.X.MathBigInt(), process.EncryptionKey.Y.MathBigInt()), vote.Address, @@ -240,7 +240,7 @@ func (a *API) newVote(w http.ResponseWriter, r *http.Request) { } // Create the ballot object ballot := &storage.Ballot{ - ProcessID: *vote.ProcessID, + ProcessID: vote.ProcessID, VoterWeight: voterWeight.MathBigInt(), // convert the ballot from TE (circom) to RTE (gnark) EncryptedBallot: vote.Ballot.FromTEtoRTE(), diff --git a/cmd/e2e-test/main.go b/cmd/e2e-test/main.go index c15f35de..97a9d09e 100644 --- a/cmd/e2e-test/main.go +++ b/cmd/e2e-test/main.go @@ -627,7 +627,7 @@ func createVote( // Return the vote ready to be sent to the sequencer return api.Vote{ - ProcessID: &wasmResult.ProcessID, + ProcessID: wasmResult.ProcessID, Address: wasmInputs.Address, Ballot: wasmResult.Ballot, BallotProof: circomProof, diff --git a/tests/census_test.go b/tests/census_test.go deleted file mode 100644 index 052a51e1..00000000 --- a/tests/census_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package tests - -import ( - "bytes" - "context" - "math/big" - "testing" - "time" - - qt "github.com/frankban/quicktest" - "github.com/vocdoni/davinci-node/api" - "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" - "github.com/vocdoni/davinci-node/internal/testutil" - "github.com/vocdoni/davinci-node/storage" - "github.com/vocdoni/davinci-node/types" - "github.com/vocdoni/davinci-node/util" -) - -func TestDynamicOffChainCensus(t *testing.T) { - numInitialVoters := 2 - c := qt.New(t) - - // Setup - ctx := t.Context() - - censusCtx, cancel := context.WithCancel(ctx) - defer cancel() - - _, port := services.API.HostPort() - cli, err := NewTestClient(port) - c.Assert(err, qt.IsNil) - - var ( - pid types.ProcessID - encryptionKey *types.EncryptionKey - ballotMode *types.BallotMode - signers []*ethereum.Signer - censusRoot []byte - censusURI string - ) - - c.Run("create process", func(c *qt.C) { - // Create census with numVoters participants - censusRoot, censusURI, signers, err = createCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainDynamicV1, numInitialVoters) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) - ballotMode = testutil.BallotModeInternal() - - // create process in sequencer - var stateRoot *types.HexBytes - pid, encryptionKey, stateRoot, err = createProcessInSequencer(services.Contracts, cli, testCensusOrigin(), censusURI, censusRoot, ballotMode) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) - - // now create process in contracts - pid2, err := createProcessInContracts(services.Contracts, testCensusOrigin(), censusURI, censusRoot, ballotMode, encryptionKey, stateRoot, numInitialVoters) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) - c.Assert(pid2.String(), qt.Equals, pid.String()) - - // create a timeout for the process creation, if it is greater than the - // test timeout use the test timeout - createProcessTimeout := time.Minute * 2 - if timeout, hasDeadline := t.Deadline(); hasDeadline { - remainingTime := time.Until(timeout) - if remainingTime < createProcessTimeout { - createProcessTimeout = remainingTime - } - } - // Wait for the process to be registered - createProcessCtx, cancel := context.WithTimeout(ctx, createProcessTimeout) - defer cancel() - - CreateProcessLoop: - for { - select { - case <-createProcessCtx.Done(): - c.Fatal("Timeout waiting for process to be created and registered") - c.FailNow() - default: - if _, err := services.Storage.Process(pid); err == nil { - break CreateProcessLoop - } - time.Sleep(time.Millisecond * 200) - } - } - t.Logf("Process ID: %s", pid.String()) - - // Wait for the process to be registered in the sequencer - for { - select { - case <-createProcessCtx.Done(): - c.Fatal("Timeout waiting for process to be registered in sequencer") - c.FailNow() - default: - if services.Sequencer.ExistsProcessID(pid) { - t.Logf("Process ID %s registered in sequencer", pid.String()) - return - } - time.Sleep(time.Millisecond * 200) - } - } - }) - - // Store the voteIDs returned from the API to check their status later - var voteIDs []types.HexBytes - var ks []*big.Int - - c.Run("create votes", func(c *qt.C) { - c.Assert(len(signers), qt.Equals, numInitialVoters) - for i := range signers { - // generate a vote for the first participant - k := util.RandomBigInt(big.NewInt(100000000), big.NewInt(9999999999999999)) - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signers[i], k) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signers[i].Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - _, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, 200) - - // Save the voteID for status checks - voteIDs = append(voteIDs, vote.VoteID) - ks = append(ks, k) - } - // Wait for the vote to be registered - t.Logf("Waiting for %d votes to be registered and aggregated", numInitialVoters) - }) - - c.Assert(ks, qt.HasLen, numInitialVoters) - c.Assert(voteIDs, qt.HasLen, numInitialVoters) - - c.Run("update the census", func(c *qt.C) { - // create a signer that is not in the census - signer, err := ethereum.NewSigner() - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create ethereum signer")) - // try to vote with the new signer, should fail - k := util.RandomBigInt(big.NewInt(100000000), big.NewInt(9999999999999999)) - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signer, k) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signer.Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - body, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, api.ErrInvalidCensusProof.HTTPstatus) - c.Assert(string(body), qt.Contains, api.ErrInvalidCensusProof.Error()) - - // create a new census including the new signer - signers = append(signers, signer) - censusRoot, censusURI, _, err = createCensusWithVoters(censusCtx, types.CensusOriginMerkleTreeOffchainDynamicV1, signers...) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) - - // update the census in the contracts - err = updateProcessCensusInContracts(services.Contracts, pid, types.Census{ - CensusOrigin: testCensusOrigin(), - CensusRoot: censusRoot, - CensusURI: censusURI, - }) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to update process census in contracts")) - - // wait to new census in the sequencer - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - for range ticker.C { - // Get the process from storage - process, err := services.Storage.Process(pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get process from storage")) - if !bytes.Equal(process.Census.CensusRoot, censusRoot) { - continue - } - t.Log("Process census root updated") - break - } - - // Make the request to cast the vote - _, status, err = cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, 200) - - // Save the voteID for status checks - voteIDs = append(voteIDs, vote.VoteID) - ks = append(ks, k) - }) - - timeoutCh := testTimeoutChan(t) - - c.Run("wait for process votes", func(c *qt.C) { - // Create a ticker to check the status of votes every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - SettledVotesLoop: - for { - select { - case <-ticker.C: - // Check that votes are settled (state transitions confirmed on blockchain) - if allSettled, failed, err := checkVoteStatus(cli, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { - c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) - if len(failed) > 0 { - hexFailed := make([]string, len(failed)) - for i, v := range failed { - hexFailed[i] = v.String() - } - t.Fatalf("Some votes failed to be settled: %v", hexFailed) - } - } - votersCount, err := votersCount(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) - if votersCount < len(voteIDs) { - continue - } - break SettledVotesLoop - case <-timeoutCh: - c.Fatalf("Timeout waiting for votes to be settled and published at contract") - } - } - t.Log("All votes settled.") - }) - - c.Run("wait for publish votes", func(c *qt.C) { - err := finishProcessOnContract(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) - results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) - c.Assert(err, qt.IsNil) - c.Logf("Results calculated: %v, waiting for onchain results...", results) - - // Create a ticker to check the status of votes every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - results, err := publishedResults(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) - if results == nil { - t.Log("Results not yet published, waiting...") - continue - } - t.Logf("Results published: %v", results) - return - case <-timeoutCh: - c.Fatalf("Timeout waiting for votes to be processed and published at contract") - } - } - }) -} diff --git a/tests/csp_census_test.go b/tests/csp_census_test.go new file mode 100644 index 00000000..0594f4bb --- /dev/null +++ b/tests/csp_census_test.go @@ -0,0 +1,148 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestCSPCensus(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + numVoters := 2 + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + c := qt.New(t) + + var ( + err error + pid types.ProcessID + stateRoot *types.HexBytes + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create process", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginCSPEdDSABN254V1, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, numVoters) + + // create process in the sequencer + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginCSPEdDSABN254V1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginCSPEdDSABN254V1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created in storage") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create votes", func(c *qt.C) { + for i, signer := range signers { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginCSPEdDSABN254V1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + c.Assert(ks, qt.HasLen, numVoters) + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be settled", numVoters) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == numVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) +} diff --git a/tests/docker/deploy.sh b/tests/docker/deploy.sh index effb8394..e0ede751 100755 --- a/tests/docker/deploy.sh +++ b/tests/docker/deploy.sh @@ -84,6 +84,8 @@ forge script \ --slow \ --optimize \ --optimizer-runs 200 \ + --gas-price 0 \ + --base-fee 0 \ -vvvv # 4) extract addresses into JSON diff --git a/tests/helpers/census.go b/tests/helpers/census.go new file mode 100644 index 00000000..9d4a11c0 --- /dev/null +++ b/tests/helpers/census.go @@ -0,0 +1,132 @@ +package helpers + +import ( + "context" + "fmt" + "math/big" + "os" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + censustest "github.com/vocdoni/davinci-node/census/test" + "github.com/vocdoni/davinci-node/crypto/csp" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/internal/testutil" + "github.com/vocdoni/davinci-node/state" + "github.com/vocdoni/davinci-node/types" + "github.com/vocdoni/davinci-node/web3" +) + +func IsCSPCensus() bool { + cspCensusEnvVar := os.Getenv(CSPCensusEnvVarName) + return strings.ToLower(cspCensusEnvVar) == "true" || cspCensusEnvVar == "1" +} + +func CurrentCensusOrigin() types.CensusOrigin { + if IsCSPCensus() { + return types.CensusOriginCSPEdDSABN254V1 + } else { + return types.CensusOriginMerkleTreeOffchainDynamicV1 + } +} + +func WrongCensusOrigin() types.CensusOrigin { + if IsCSPCensus() { + return types.CensusOriginMerkleTreeOffchainStaticV1 + } else { + return types.CensusOriginCSPEdDSABN254V1 + } +} + +func NewCensusWithRandomVoters(ctx context.Context, origin types.CensusOrigin, nVoters int) ([]byte, string, []*ethereum.Signer, error) { + // Generate random participants + signers := []*ethereum.Signer{} + votes := []state.Vote{} + for range nVoters { + signer, err := ethereum.NewSigner() + if err != nil { + return nil, "", nil, fmt.Errorf("failed to generate signer: %w", err) + } + signers = append(signers, signer) + votes = append(votes, state.Vote{ + Address: signer.Address().Big(), + Weight: big.NewInt(testutil.Weight), + }) + } + + if origin.IsCSP() { + eddsaCSP, err := csp.New(origin, []byte(LocalCSPSeed)) + if err != nil { + return nil, "", nil, fmt.Errorf("failed to create CSP: %w", err) + } + root := eddsaCSP.CensusRoot() + if root == nil { + return nil, "", nil, fmt.Errorf("census root is nil") + } + return root.Root, "http://myowncsp.test", signers, nil + } else { + censusRoot, censusURI, err := censustest.NewCensus3MerkleTreeForTest(ctx, origin, votes, DefaultCensus3URL) + if err != nil { + return nil, "", nil, fmt.Errorf("failed to serve census merkle tree: %w", err) + } + return censusRoot.Bytes(), censusURI, signers, nil + } +} + +func NewCensusWithVoters(ctx context.Context, origin types.CensusOrigin, signers ...*ethereum.Signer) ([]byte, string, []*ethereum.Signer, error) { + // Generate random participants + votes := []state.Vote{} + for _, signer := range signers { + votes = append(votes, state.Vote{ + Address: signer.Address().Big(), + Weight: big.NewInt(testutil.Weight), + }) + } + + if origin.IsCSP() { + eddsaCSP, err := csp.New(origin, []byte(LocalCSPSeed)) + if err != nil { + return nil, "", nil, fmt.Errorf("failed to create CSP: %w", err) + } + root := eddsaCSP.CensusRoot() + if root == nil { + return nil, "", nil, fmt.Errorf("census root is nil") + } + return root.Root, "http://myowncsp.test", signers, nil + } else { + censusRoot, censusURI, err := censustest.NewCensus3MerkleTreeForTest(ctx, origin, votes, DefaultCensus3URL) + if err != nil { + return nil, "", nil, fmt.Errorf("failed to serve census merkle tree: %w", err) + } + return censusRoot.Bytes(), censusURI, signers, nil + } +} + +func CreateCensusProof(origin types.CensusOrigin, pid types.ProcessID, key []byte) (types.CensusProof, error) { + if origin.IsCSP() { + weight := new(types.BigInt).SetUint64(testutil.Weight) + eddsaCSP, err := csp.New(types.CensusOriginCSPEdDSABN254V1, []byte(LocalCSPSeed)) + if err != nil { + return types.CensusProof{}, fmt.Errorf("failed to create CSP: %w", err) + } + cspProof, err := eddsaCSP.GenerateProof(pid, common.BytesToAddress(key), weight) + if err != nil { + return types.CensusProof{}, fmt.Errorf("failed to generate CSP proof: %w", err) + } + return *cspProof, nil + } + return types.CensusProof{}, nil +} + +func UpdateCensusOnChain( + contracts *web3.Contracts, + pid types.ProcessID, + census types.Census, +) error { + txHash, err := contracts.SetProcessCensus(pid, census) + if err != nil { + return fmt.Errorf("failed to update process census: %w", err) + } + return contracts.WaitTxByHash(*txHash, time.Second*15) +} diff --git a/tests/helpers/common.go b/tests/helpers/common.go new file mode 100644 index 00000000..c62a0bfd --- /dev/null +++ b/tests/helpers/common.go @@ -0,0 +1,53 @@ +package helpers + +import ( + "context" + "fmt" + "os" + "testing" + "time" +) + +func IsDebugTest() bool { + return os.Getenv("DEBUG") != "" && os.Getenv("DEBUG") != "false" +} + +func MaxTestTimeout(t *testing.T) time.Duration { + t.Helper() + + // Set up timeout based on context deadline + if deadline, hasDeadline := t.Deadline(); hasDeadline { + // If context has a deadline, set timeout to 15 seconds before it + // to allow for clean shutdown and error reporting + remainingTime := time.Until(deadline) + timeoutBuffer := 15 * time.Second + + // If we have less than the buffer time left, use half of the remaining time + if remainingTime <= timeoutBuffer { + timeoutBuffer = remainingTime / 2 + } + + effectiveTimeout := remainingTime - timeoutBuffer + return effectiveTimeout + } + // No deadline set, use a reasonable default + if IsDebugTest() { + return 50 * time.Minute + } + return 20 * time.Minute +} + +func WaitUntilCondition(ctx context.Context, interval time.Duration, condition func() bool) error { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for condition") + case <-ticker.C: + if condition() { + return nil + } + } + } +} diff --git a/tests/helpers/constants.go b/tests/helpers/constants.go new file mode 100644 index 00000000..796709ed --- /dev/null +++ b/tests/helpers/constants.go @@ -0,0 +1,37 @@ +package helpers + +import ( + "fmt" + "time" + + "github.com/vocdoni/davinci-node/util" +) + +const ( + // first account private key created by anvil with default mnemonic + LocalAccountPrivKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + LocalCSPSeed = "1f1e0cd27b4ecd1b71b6333790864ace2870222c" + WorkerSeed = "test-seed" + WorkerTokenExpiration = 24 * time.Hour + WorkerTimeout = time.Second * 5 + // envarionment variable names + DeployerServerPortEnvVarName = "DEPLOYER_SERVER" // environment variable name for deployer server port + ContractsBranchNameEnvVarName = "SEQUENCER_CONTRACTS_BRANCH" // environment variable name for z-contracts branch + ContractsCommitHashEnvVarName = "SEQUENCER_CONTRACTS_COMMIT" // environment variable name for z-contracts commit hash + PrivKeyEnvVarName = "SEQUENCER_PRIV_KEY" // environment variable name for private key + RPCUrlEnvVarName = "SEQUENCER_RPC_URL" // environment variable name for RPC URL + AnvilPortEnvVarName = "ANVIL_PORT_RPC_HTTP" // environment variable name for Anvil port + OrgRegistryEnvVarName = "SEQUENCER_ORGANIZATION_REGISTRY" // environment variable name for organization registry + ProcessRegistryEnvVarName = "SEQUENCER_PROCESS_REGISTRY" // environment variable name for process registry + ResultsVerifierEnvVarName = "SEQUENCER_RESULTS_ZK_VERIFIER" // environment variable name for results zk verifier + StateTransitionVerifierEnvVarName = "SEQUENCER_STATE_TRANSITION_ZK_VERIFIER" // environment variable name for state transition zk verifier + CSPCensusEnvVarName = "CSP_CENSUS" // environment variable name to select between csp or merkle tree census (by default merkle tree) + + DefaultBatchTimeWindow = 45 * time.Second // default batch time window for sequencer +) + +var ( + DefaultAPIPort = util.RandomInt(40000, 60000) + DefaultCensus3Port = util.RandomInt(40000, 60000) + DefaultCensus3URL = fmt.Sprintf("http://localhost:%d", DefaultCensus3Port) +) diff --git a/tests/helpers/org.go b/tests/helpers/org.go new file mode 100644 index 00000000..816e2123 --- /dev/null +++ b/tests/helpers/org.go @@ -0,0 +1,26 @@ +package helpers + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/vocdoni/davinci-node/types" + "github.com/vocdoni/davinci-node/web3" +) + +func CreateOrganization(contracts *web3.Contracts) (common.Address, error) { + orgAddr := contracts.AccountAddress() + txHash, err := contracts.CreateOrganization(orgAddr, &types.OrganizationInfo{ + Name: fmt.Sprintf("Vocdoni test %x", orgAddr[:4]), + MetadataURI: "https://vocdoni.io", + }) + if err != nil { + return common.Address{}, fmt.Errorf("failed to create organization: %w", err) + } + + if err = contracts.WaitTxByHash(txHash, time.Second*30); err != nil { + return common.Address{}, fmt.Errorf("failed to wait for organization creation transaction: %w", err) + } + return orgAddr, nil +} diff --git a/tests/helpers/process.go b/tests/helpers/process.go new file mode 100644 index 00000000..4e089742 --- /dev/null +++ b/tests/helpers/process.go @@ -0,0 +1,166 @@ +package helpers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/api/client" + "github.com/vocdoni/davinci-node/types" + "github.com/vocdoni/davinci-node/web3" +) + +func NewProcess( + contracts *web3.Contracts, + cli *client.HTTPclient, + censusOrigin types.CensusOrigin, + censusURI string, + censusRoot []byte, + ballotMode *types.BallotMode, +) (types.ProcessID, *types.EncryptionKey, *types.HexBytes, error) { + // Get the next process ID from the contracts + processID, err := contracts.NextProcessID(contracts.AccountAddress()) + if err != nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("failed to get next process ID: %w", err) + } + + // Sign the process creation request + signature, err := contracts.SignMessage(fmt.Appendf(nil, types.NewProcessMessageToSign, processID.String())) + if err != nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("failed to sign message: %w", err) + } + + process := &types.ProcessSetup{ + ProcessID: processID, + Census: &types.Census{ + CensusOrigin: censusOrigin, + CensusURI: censusURI, + CensusRoot: censusRoot, + }, + BallotMode: ballotMode, + Signature: signature, + } + + body, code, err := cli.Request(http.MethodPost, process, nil, api.ProcessesEndpoint) + if err != nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("failed to create process: %w", err) + } + if code != http.StatusOK { + return types.ProcessID{}, nil, nil, fmt.Errorf("unexpected status code creating process: %d, body: %s", code, string(body)) + } + + var resp types.ProcessSetupResponse + err = json.NewDecoder(bytes.NewReader(body)).Decode(&resp) + if err != nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("failed to decode process response: %w", err) + } + if resp.ProcessID == nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("process ID is nil") + } + if resp.EncryptionPubKey[0] == nil || resp.EncryptionPubKey[1] == nil { + return types.ProcessID{}, nil, nil, fmt.Errorf("encryption public key is nil") + } + + encryptionKeys := &types.EncryptionKey{ + X: resp.EncryptionPubKey[0], + Y: resp.EncryptionPubKey[1], + } + return processID, encryptionKeys, &resp.StateRoot, nil +} + +func NewProcessOnChain( + contracts *web3.Contracts, + censusOrigin types.CensusOrigin, + censusURI string, + censusRoot []byte, + ballotMode *types.BallotMode, + encryptionKey *types.EncryptionKey, + stateRoot *types.HexBytes, + numVoters int, + duration ...time.Duration, +) (types.ProcessID, error) { + finalDuration := time.Hour + if len(duration) > 0 { + finalDuration = duration[0] + } + + pid, txHash, err := contracts.CreateProcess(&types.Process{ + Status: types.ProcessStatusReady, + OrganizationId: contracts.AccountAddress(), + EncryptionKey: encryptionKey, + StateRoot: stateRoot.BigInt(), + StartTime: time.Now().Add(1 * time.Minute), + Duration: finalDuration, + MaxVoters: types.NewInt(numVoters), + MetadataURI: "https://example.com/metadata", + BallotMode: ballotMode, + Census: &types.Census{ + CensusRoot: censusRoot, + CensusURI: censusURI, + CensusOrigin: censusOrigin, + }, + }) + if err != nil { + return types.ProcessID{}, fmt.Errorf("failed to create process: %w", err) + } + return pid, contracts.WaitTxByHash(*txHash, time.Second*15) +} + +func UpdateMaxVotersOnChain( + contracts *web3.Contracts, + pid types.ProcessID, + numVoters int, +) error { + currentProcess, err := contracts.Process(pid) + if err != nil { + return fmt.Errorf("failed to get current process: %w", err) + } + currentMaxVoters := currentProcess.MaxVoters.MathBigInt().Int64() + if numVoters < int(currentMaxVoters) { + return fmt.Errorf("new max voters (%d) is less than current max voters (%d)", numVoters, currentMaxVoters) + } + txHash, err := contracts.SetProcessMaxVoters(pid, types.NewInt(numVoters)) + if err != nil { + return fmt.Errorf("failed to set process max voters: %w", err) + } + return contracts.WaitTxByHash(*txHash, time.Second*15) +} + +func FetchProcessVotersCountOnChain(contracts *web3.Contracts, pid types.ProcessID) (int, error) { + process, err := contracts.Process(pid) + if err != nil { + return 0, fmt.Errorf("failed to get process: %w", err) + } + if process == nil || process.VotersCount == nil { + return 0, nil + } + return int(process.VotersCount.MathBigInt().Int64()), nil +} + +func FetchProcessOnChainOverwrittenVotesCount(contracts *web3.Contracts, pid types.ProcessID) (int, error) { + process, err := contracts.Process(pid) + if err != nil { + return 0, fmt.Errorf("failed to get process: %w", err) + } + if process == nil || process.OverwrittenVotesCount == nil { + return 0, nil + } + return int(process.OverwrittenVotesCount.MathBigInt().Int64()), nil +} + +func FinishProcessOnChain(contracts *web3.Contracts, pid types.ProcessID) error { + txHash, err := contracts.SetProcessStatus(pid, types.ProcessStatusEnded) + if err != nil { + return fmt.Errorf("failed to set process status: %w", err) + } + if txHash == nil { + return fmt.Errorf("transaction hash is nil") + } + if err = contracts.WaitTxByHash(*txHash, time.Second*30); err != nil { + return fmt.Errorf("failed to wait for transaction: %w", err) + } + return nil +} diff --git a/tests/helpers/results.go b/tests/helpers/results.go new file mode 100644 index 00000000..b9da1db9 --- /dev/null +++ b/tests/helpers/results.go @@ -0,0 +1,19 @@ +package helpers + +import ( + "fmt" + + "github.com/vocdoni/davinci-node/types" + "github.com/vocdoni/davinci-node/web3" +) + +func FetchResultsOnChain(contracts *web3.Contracts, pid types.ProcessID) ([]*types.BigInt, error) { + process, err := contracts.Process(pid) + if err != nil { + return nil, fmt.Errorf("failed to get process: %w", err) + } + if process == nil || process.Status != types.ProcessStatusResults || len(process.Result) == 0 { + return nil, nil + } + return process.Result, nil +} diff --git a/tests/helpers/service.go b/tests/helpers/service.go new file mode 100644 index 00000000..96cb1169 --- /dev/null +++ b/tests/helpers/service.go @@ -0,0 +1,483 @@ +package helpers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/consensys/gnark/logger" + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + tc "github.com/testcontainers/testcontainers-go/modules/compose" + c3config "github.com/vocdoni/census3-bigquery/config" + c3service "github.com/vocdoni/census3-bigquery/service" + "github.com/vocdoni/davinci-node/api/client" + "github.com/vocdoni/davinci-node/config" + "github.com/vocdoni/davinci-node/db" + "github.com/vocdoni/davinci-node/db/metadb" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/sequencer" + "github.com/vocdoni/davinci-node/service" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/util" + "github.com/vocdoni/davinci-node/web3" + "github.com/vocdoni/davinci-node/web3/txmanager" + "github.com/vocdoni/davinci-node/workers" + "golang.org/x/mod/modfile" +) + +// TestServices struct holds all test services +type TestServices struct { + API *service.APIService + Census3 *c3service.Service + Sequencer *sequencer.Sequencer + CensusDownloader *service.CensusDownloader + Storage *storage.Storage + Contracts *web3.Contracts + HTTPClient *client.HTTPclient +} + +func NewTestServices( + ctx context.Context, + tempDir string, + workerSecret string, + workerTokenExpiration time.Duration, + workerTimeout time.Duration, + banRules *workers.WorkerBanRules, +) (*TestServices, func(), error) { + // Initialize census3 service + c3srv, c3cleanup, err := setupCensusService() + if err != nil { + return nil, nil, fmt.Errorf("failed to setup census3 service: %w", err) + } + // Initialize the web3 contracts + contracts, web3Cleanup, err := setupWeb3(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to setup web3: %w", err) + } + + kv, err := metadb.New(db.TypePebble, tempDir) + if err != nil { + web3Cleanup() // Clean up web3 if db creation fails + return nil, nil, fmt.Errorf("failed to create database: %w", err) + } + stg := storage.New(kv) + + services := &TestServices{ + Census3: c3srv, + Storage: stg, + Contracts: contracts, + } + + // Start sequencer service + sequencer.AggregatorTickerInterval = time.Second * 2 + sequencer.NewProcessMonitorInterval = time.Second * 5 + vp := service.NewSequencer(stg, contracts, DefaultBatchTimeWindow, nil) + seqCtx, seqCancel := context.WithCancel(ctx) + if err := vp.Start(seqCtx); err != nil { + seqCancel() + web3Cleanup() // Clean up web3 if sequencer fails to start + return nil, nil, fmt.Errorf("failed to start sequencer: %w", err) + } + services.Sequencer = vp.Sequencer + + if IsDebugTest() { + logger.Set(zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"}).With().Timestamp().Logger()) + log.Info("Debug prover is disabled in non-testing context") + } + + // Start census downloader + cd := service.NewCensusDownloader(contracts, services.Storage, service.CensusDownloaderConfig{ + CleanUpInterval: time.Second * 5, + Expiration: time.Minute * 30, + Cooldown: time.Second * 10, + Attempts: 5, + }) + if err := cd.Start(ctx); err != nil { + vp.Stop() + seqCancel() + web3Cleanup() + return nil, nil, fmt.Errorf("failed to start census downloader: %w", err) + } + services.CensusDownloader = cd + + // Start StateSync + stateSync := service.NewStateSync(contracts, stg) + if err := stateSync.Start(ctx); err != nil { + cd.Stop() + vp.Stop() + seqCancel() + web3Cleanup() // Clean up web3 if process monitor fails to start + return nil, nil, fmt.Errorf("failed to start state sync: %v", err) + } + + // Start process monitor + pm := service.NewProcessMonitor(contracts, stg, cd, stateSync, time.Second*2) + if err := pm.Start(ctx); err != nil { + cd.Stop() + vp.Stop() + seqCancel() + web3Cleanup() // Clean up web3 if process monitor fails to start + return nil, nil, fmt.Errorf("failed to start process monitor: %w", err) + } + // Start API service + web3Conf := config.DavinciWeb3Config{ + ProcessRegistrySmartContract: contracts.ContractsAddresses.ProcessRegistry.String(), + OrganizationRegistrySmartContract: contracts.ContractsAddresses.OrganizationRegistry.String(), + ResultsZKVerifier: contracts.ContractsAddresses.ResultsZKVerifier.String(), + StateTransitionZKVerifier: contracts.ContractsAddresses.StateTransitionZKVerifier.String(), + } + api, err := setupAPI(ctx, stg, workerSecret, workerTokenExpiration, workerTimeout, banRules, web3Conf) + if err != nil { + pm.Stop() + cd.Stop() + vp.Stop() + seqCancel() + web3Cleanup() // Clean up web3 if API fails to start + return nil, nil, fmt.Errorf("failed to setup API: %w", err) + } + services.API = api + services.HTTPClient, err = httpClient(DefaultAPIPort) + if err != nil { + api.Stop() + pm.Stop() + cd.Stop() + vp.Stop() + seqCancel() + web3Cleanup() + return nil, nil, fmt.Errorf("failed to create HTTP client: %w", err) + } + + // Create a combined cleanup function + cleanup := func() { + seqCancel() + api.Stop() + cd.Stop() + pm.Stop() + vp.Stop() + stg.Close() + c3cleanup() + web3Cleanup() + } + + return services, cleanup, nil +} + +// httpClient creates a new API client for testing. +func httpClient(port int) (*client.HTTPclient, error) { + return client.New(fmt.Sprintf("http://127.0.0.1:%d", port)) +} + +// setupAPI creates and starts a new API server for testing. +// It returns the server port. +func setupAPI( + ctx context.Context, + db *storage.Storage, + workerSeed string, + workerTokenExpiration time.Duration, + workerTimeout time.Duration, + banRules *workers.WorkerBanRules, + web3Conf config.DavinciWeb3Config, +) (*service.APIService, error) { + api := service.NewAPI(db, "127.0.0.1", DefaultAPIPort, "test", web3Conf, false) + api.SetWorkerConfig(workerSeed, workerTokenExpiration, workerTimeout, banRules) + if err := api.Start(ctx); err != nil { + return nil, err + } + + // Wait for the HTTP server to start + time.Sleep(500 * time.Millisecond) + return api, nil +} + +// setupCensusService creates and starts a new census3-bigquery service for +// testing. +func setupCensusService() (*c3service.Service, func(), error) { + // create temp dir for census3-bigquery + tempDir, err := os.MkdirTemp("", "census3-bigquery-test-") + if err != nil { + return nil, nil, fmt.Errorf("failed to create temp dir for census3-bigquery: %w", err) + } + + srv, err := c3service.New(&c3config.Config{ + APIPort: DefaultCensus3Port, + DataDir: tempDir, + MaxCensusSize: 1000000, + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to create census3-bigquery service: %w", err) + } + + go func() { + if err := srv.Start(); err != nil { + log.Errorw(err, "census3-bigquery service exited with error") + } + }() + return srv, func() { + srv.Stop() + if err := os.RemoveAll(tempDir); err != nil { + log.Warnw("failed to remove census3-bigquery temp dir", "error", err) + } + }, nil +} + +// setupWeb3 sets up the web3 contracts for testing. It deploys the contracts +// if the environment variables are not set, if they are set it loads the +// contracts from the environment variables. It returns the contracts object +// and a cleanup function that should be called when done. +func setupWeb3(ctx context.Context) (*web3.Contracts, func(), error) { + // Get the environment variables + var ( + privKey = os.Getenv(PrivKeyEnvVarName) + rpcUrl = os.Getenv(RPCUrlEnvVarName) + orgRegistryAddr = os.Getenv(OrgRegistryEnvVarName) + processRegistryAddr = os.Getenv(ProcessRegistryEnvVarName) + stateTransitionZKVerifierAddr = os.Getenv(StateTransitionVerifierEnvVarName) + resultsZKVerifierAddr = os.Getenv(ResultsVerifierEnvVarName) + ) + // Check if the environment variables are set to run the tests over local + // geth node or remote blockchain environment + localEnv := privKey == "" || rpcUrl == "" || orgRegistryAddr == "" || + processRegistryAddr == "" || resultsZKVerifierAddr == "" || stateTransitionZKVerifierAddr == "" + + // Store cleanup functions + var cleanupFuncs []func() + cleanup := func() { + // Execute cleanup functions in reverse order + for i := len(cleanupFuncs) - 1; i >= 0; i-- { + cleanupFuncs[i]() + } + } + + var deployerUrl string + if localEnv { + // Generate a random port for geth HTTP RPC + anvilPort := util.RandomInt(10000, 20000) + rpcUrl = fmt.Sprintf("http://localhost:%d", anvilPort) + // Set environment variables for docker-compose in the process environment + composeEnv := make(map[string]string) + composeEnv[AnvilPortEnvVarName] = fmt.Sprintf("%d", anvilPort) + composeEnv[DeployerServerPortEnvVarName] = fmt.Sprintf("%d", anvilPort+1) + composeEnv[PrivKeyEnvVarName] = LocalAccountPrivKey + + // get branch and commit from the environment variables + if branchName := os.Getenv(ContractsBranchNameEnvVarName); branchName != "" { + composeEnv[ContractsBranchNameEnvVarName] = branchName + } + if commitHash := os.Getenv(ContractsCommitHashEnvVarName); commitHash != "" { + composeEnv[ContractsCommitHashEnvVarName] = commitHash + } else { + // get it from the go mod file + modData, err := os.ReadFile("../go.mod") + if err != nil { + return nil, nil, fmt.Errorf("failed to read go.mod file: %w", err) + } + modFile, err := modfile.Parse("go.mod", modData, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse go.mod file: %w", err) + } + // get the commit hash from the replace directive + for _, r := range modFile.Require { + if r.Mod.Path != "github.com/vocdoni/davinci-contracts" { + continue + } + if versionParts := strings.Split(r.Mod.Version, "-"); len(versionParts) == 3 { + composeEnv[ContractsCommitHashEnvVarName] = versionParts[2] + break + } + if versionParts := strings.Split(r.Mod.Version, "."); len(versionParts) == 3 { + composeEnv[ContractsCommitHashEnvVarName] = r.Mod.Version + break + } + return nil, nil, fmt.Errorf("cannot parse davinci-contracts version: %s", r.Mod.Version) + + } + } + + log.Infow("deploying contracts in local environment", + "commit", composeEnv[ContractsCommitHashEnvVarName], + "branch", composeEnv[ContractsBranchNameEnvVarName]) + + // Create docker-compose instance + compose, err := tc.NewDockerCompose("docker/docker-compose.yml") + if err != nil { + return nil, nil, fmt.Errorf("failed to create docker compose: %w", err) + } + ctx2, cancel := context.WithCancel(ctx) + // Register cleanup for context cancellation + cleanupFuncs = append(cleanupFuncs, cancel) + + // Start docker-compose + log.Infow("starting Anvil docker compose", "gethPort", anvilPort) + err = compose.WithEnv(composeEnv).Up(ctx2, tc.Wait(true), tc.RemoveOrphans(true)) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to start docker compose: %w", err) + } + + // Register cleanup for docker compose shutdown + cleanupFuncs = append(cleanupFuncs, func() { + downCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + if downErr := compose.Down(downCtx, tc.RemoveOrphans(true), tc.RemoveVolumes(true)); downErr != nil { + log.Warnw("failed to stop docker compose", "error", downErr) + } + }) + + deployerCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + // Get the enpoint of the deployer service + deployerContainer, err := compose.ServiceContainer(deployerCtx, "deployer") + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to get deployer container: %w", err) + } + deployerUrl, err = deployerContainer.Endpoint(deployerCtx, "http") + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to get deployer endpoint: %w", err) + } + } + + // Wait for the RPC to be ready + err := web3.WaitReadyRPC(ctx, rpcUrl) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to wait for RPC: %w", err) + } + + // Initialize the contracts object + contracts, err := web3.New([]string{rpcUrl}, "", 1.0) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to create web3 contracts: %w", err) + } + + // Define contracts addresses or deploy them + if localEnv { + type deployerResponse struct { + Txs []struct { + ContractName string `json:"contractName"` + ContractAddress string `json:"contractAddress"` + } `json:"transactions"` + } + + // Wait until contracts are deployed and get their addresses from + // deployer + contractsCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + var contractsAddresses *web3.Addresses + for contractsAddresses == nil { + select { + case <-contractsCtx.Done(): + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("timeout waiting for contracts to be deployed") + case <-time.After(5 * time.Second): + // Check if the contracts are deployed making an http request + // to /addresses.json + endpoint := fmt.Sprintf("%s/addresses.json", deployerUrl) + res, err := http.Get(endpoint) + if err != nil { + log.Infow("waiting for contracts to be deployed", + "err", err, + "deployUrl", endpoint) + continue + } + if res.StatusCode != http.StatusOK { + if err := res.Body.Close(); err != nil { + log.Warnw("failed to close deployer response body", "error", err) + } + log.Infow("waiting for contracts to be deployed", + "status", res.StatusCode, + "deployUrl", endpoint) + continue + } + // Decode the response + var deployerResp deployerResponse + err = json.NewDecoder(res.Body).Decode(&deployerResp) + if err := res.Body.Close(); err != nil { + log.Warnw("failed to close deployer response body", "error", err) + } + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to decode deployer response: %w", err) + } + contractsAddresses = new(web3.Addresses) + log.Infow("contracts addresses from deployer", + "logs", deployerResp.Txs) + for _, tx := range deployerResp.Txs { + switch tx.ContractName { + case "OrganizationRegistry": + contractsAddresses.OrganizationRegistry = common.HexToAddress(tx.ContractAddress) + case "ProcessRegistry": + contractsAddresses.ProcessRegistry = common.HexToAddress(tx.ContractAddress) + case "StateTransitionVerifierGroth16": + contractsAddresses.StateTransitionZKVerifier = common.HexToAddress(tx.ContractAddress) + case "ResultsVerifierGroth16": + contractsAddresses.ResultsZKVerifier = common.HexToAddress(tx.ContractAddress) + default: + log.Infow("unknown contract name", "name", tx.ContractName) + } + } + } + } + // Set the private key for the sequencer + err = contracts.SetAccountPrivateKey(util.TrimHex(LocalAccountPrivKey)) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to set account private key: %w", err) + } + // Load the contracts addresses into the contracts object + err = contracts.LoadContracts(contractsAddresses) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to load contracts: %w", err) + } + log.Infow("contracts deployed and loaded", + "chainId", contracts.ChainID, + "addresses", contractsAddresses) + } else { + // Set the private key for the sequencer + err = contracts.SetAccountPrivateKey(util.TrimHex(privKey)) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to set account private key: %w", err) + } + // Create the contracts object with the addresses from the environment + err = contracts.LoadContracts(&web3.Addresses{ + OrganizationRegistry: common.HexToAddress(orgRegistryAddr), + ProcessRegistry: common.HexToAddress(processRegistryAddr), + ResultsZKVerifier: common.HexToAddress(resultsZKVerifierAddr), + StateTransitionZKVerifier: common.HexToAddress(stateTransitionZKVerifierAddr), + }) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to load contracts: %w", err) + } + } + + // Start the transaction manager + txm, err := txmanager.New(ctx, contracts.Web3Pool(), contracts.Client(), contracts.Signer(), txmanager.DefaultConfig(contracts.ChainID)) + if err != nil { + cleanup() // Clean up what we've done so far + return nil, nil, fmt.Errorf("failed to create transaction manager: %w", err) + } + txm.Start(ctx) + contracts.SetTxManager(txm) + cleanupFuncs = append(cleanupFuncs, func() { + txm.Stop() + }) + // Set contracts ABIs + contracts.ContractABIs = &web3.ContractABIs{ + ProcessRegistry: contracts.ProcessRegistryABI(), + OrganizationRegistry: contracts.OrganizationRegistryABI(), + StateTransitionZKVerifier: contracts.StateTransitionVerifierABI(), + ResultsZKVerifier: contracts.ResultsVerifierABI(), + } + // Return the contracts object and cleanup function + return contracts, cleanup, nil +} diff --git a/tests/helpers/vote.go b/tests/helpers/vote.go new file mode 100644 index 00000000..01b4bab0 --- /dev/null +++ b/tests/helpers/vote.go @@ -0,0 +1,157 @@ +package helpers + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/api/client" + "github.com/vocdoni/davinci-node/circuits/ballotproof" + ballotprooftest "github.com/vocdoni/davinci-node/circuits/test/ballotproof" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/internal/testutil" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/types" + "github.com/vocdoni/davinci-node/util/circomgnark" +) + +func NewVote(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey, privKey *ethereum.Signer, k *big.Int, fields []*types.BigInt) (api.Vote, error) { + var err error + // emulate user inputs + address := ethcrypto.PubkeyToAddress(privKey.PublicKey) + if k == nil { + k, err = elgamal.RandK() + if err != nil { + return api.Vote{}, fmt.Errorf("failed to generate random k: %w", err) + } + } + // set voter weight + voterWeight := new(types.BigInt).SetInt(testutil.Weight) + // compose wasm inputs + wasmInputs := &ballotproof.BallotProofInputs{ + Address: address.Bytes(), + ProcessID: pid, + EncryptionKey: []*types.BigInt{encKey.X, encKey.Y}, + K: new(types.BigInt).SetBigInt(k), + BallotMode: bm, + Weight: voterWeight, + FieldValues: fields, + } + // generate the inputs for the ballot proof circuit + wasmResult, err := ballotproof.GenerateBallotProofInputs(wasmInputs) + if err != nil { + return api.Vote{}, fmt.Errorf("failed to generate ballot proof inputs: %w", err) + } + // encode the inputs to json + encodedCircomInputs, err := json.Marshal(wasmResult.CircomInputs) + if err != nil { + return api.Vote{}, fmt.Errorf("failed to marshal circom inputs: %w", err) + } + // generate the proof using the circom circuit + rawProof, pubInputs, err := ballotprooftest.CompileAndGenerateProofForTest(encodedCircomInputs) + if err != nil { + return api.Vote{}, fmt.Errorf("failed to compile and generate proof: %w", err) + } + // convert the proof to gnark format + circomProof, _, err := circomgnark.UnmarshalCircom(rawProof, pubInputs) + if err != nil { + return api.Vote{}, fmt.Errorf("failed to unmarshal circom proof: %w", err) + } + // sign the hash of the circuit inputs + signature, err := ballotprooftest.SignECDSAForTest(privKey, wasmResult.VoteID) + if err != nil { + return api.Vote{}, fmt.Errorf("failed to sign ECDSA: %w", err) + } + // return the vote ready to be sent to the sequencer + return api.Vote{ + ProcessID: wasmResult.ProcessID, + Address: wasmInputs.Address, + VoteID: wasmResult.VoteID, + Ballot: wasmResult.Ballot, + BallotProof: circomProof, + BallotInputsHash: wasmResult.BallotInputsHash, + Signature: signature.Bytes(), + }, nil +} + +func NewVoteWithRandomFields(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey, privKey *ethereum.Signer, k *big.Int) (api.Vote, error) { + // generate random ballot fields + randFields := ballotprooftest.GenBallotFieldsForTest( + int(bm.NumFields), + int(bm.MaxValue.MathBigInt().Int64()), + int(bm.MinValue.MathBigInt().Int64()), + bm.UniqueValues) + // cast fields to types.BigInt + fields := []*types.BigInt{} + for _, f := range randFields { + fields = append(fields, (*types.BigInt)(f)) + } + return NewVote(pid, bm, encKey, privKey, k, fields) +} + +func NewVoteFromNonCensusVoter(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey) (api.Vote, error) { + privKey, err := ethereum.NewSigner() + if err != nil { + return api.Vote{}, fmt.Errorf("failed to generate signer: %w", err) + } + k, err := elgamal.RandK() + if err != nil { + return api.Vote{}, fmt.Errorf("failed to generate random k: %w", err) + } + return NewVoteWithRandomFields(pid, bm, encKey, privKey, k) +} + +func EnsureVotesStatus(cli *client.HTTPclient, pid types.ProcessID, voteIDs []types.HexBytes, expectedStatus string) (bool, []types.HexBytes, error) { + // Check vote status and return whether all votes have the expected status + allExpectedStatus := true + failed := []types.HexBytes{} + + // Check status for each vote + for _, voteID := range voteIDs { + // Construct the status endpoint URL + statusEndpoint := api.EndpointWithParam( + api.EndpointWithParam(api.VoteStatusEndpoint, + api.ProcessURLParam, pid.String()), + api.VoteIDURLParam, voteID.String()) + + // Make the request to get the vote status + body, statusCode, err := cli.Request("GET", nil, nil, statusEndpoint) + if err != nil { + return false, nil, fmt.Errorf("failed to request vote status: %w", err) + } + if statusCode != 200 { + return false, nil, fmt.Errorf("unexpected status code: %d", statusCode) + } + + // Parse the response body to get the status + var statusResponse api.VoteStatusResponse + err = json.NewDecoder(bytes.NewReader(body)).Decode(&statusResponse) + if err != nil { + return false, nil, fmt.Errorf("failed to decode status response: %w", err) + } + + // Verify the status is valid + if statusResponse.Status == "" { + return false, nil, fmt.Errorf("status is empty") + } + + // Check if the vote has the expected status + switch statusResponse.Status { + case storage.VoteIDStatusName(storage.VoteIDStatusError): + allExpectedStatus = allExpectedStatus && (expectedStatus == storage.VoteIDStatusName(storage.VoteIDStatusError)) + if expectedStatus != storage.VoteIDStatusName(storage.VoteIDStatusError) { + failed = append(failed, voteID) + } + case expectedStatus: + allExpectedStatus = allExpectedStatus && true + default: + allExpectedStatus = false + } + } + + return allExpectedStatus, failed, nil +} diff --git a/tests/helpers_test.go b/tests/helpers_test.go deleted file mode 100644 index 148d3e5b..00000000 --- a/tests/helpers_test.go +++ /dev/null @@ -1,1036 +0,0 @@ -package tests - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "math/big" - "net/http" - "os" - "strings" - "testing" - "time" - - "github.com/consensys/gnark/logger" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/rs/zerolog" - tc "github.com/testcontainers/testcontainers-go/modules/compose" - c3config "github.com/vocdoni/census3-bigquery/config" - c3service "github.com/vocdoni/census3-bigquery/service" - "github.com/vocdoni/davinci-node/api" - "github.com/vocdoni/davinci-node/api/client" - censustest "github.com/vocdoni/davinci-node/census/test" - "github.com/vocdoni/davinci-node/circuits/ballotproof" - ballotprooftest "github.com/vocdoni/davinci-node/circuits/test/ballotproof" - "github.com/vocdoni/davinci-node/config" - "github.com/vocdoni/davinci-node/crypto/csp" - "github.com/vocdoni/davinci-node/crypto/elgamal" - "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" - "github.com/vocdoni/davinci-node/db" - "github.com/vocdoni/davinci-node/db/metadb" - "github.com/vocdoni/davinci-node/internal/testutil" - "github.com/vocdoni/davinci-node/log" - "github.com/vocdoni/davinci-node/sequencer" - "github.com/vocdoni/davinci-node/service" - "github.com/vocdoni/davinci-node/state" - "github.com/vocdoni/davinci-node/storage" - "github.com/vocdoni/davinci-node/types" - "github.com/vocdoni/davinci-node/util" - "github.com/vocdoni/davinci-node/util/circomgnark" - "github.com/vocdoni/davinci-node/web3" - "github.com/vocdoni/davinci-node/web3/txmanager" - "github.com/vocdoni/davinci-node/workers" - "golang.org/x/mod/modfile" -) - -const ( - // first account private key created by anvil with default mnemonic - testLocalAccountPrivKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - testLocalCSPSeed = "1f1e0cd27b4ecd1b71b6333790864ace2870222c" - // envarionment variable names - deployerServerPortEnvVarName = "DEPLOYER_SERVER" // environment variable name for deployer server port - contractsBranchNameEnvVarName = "SEQUENCER_CONTRACTS_BRANCH" // environment variable name for z-contracts branch - contractsCommitHashEnvVarName = "SEQUENCER_CONTRACTS_COMMIT" // environment variable name for z-contracts commit hash - privKeyEnvVarName = "SEQUENCER_PRIV_KEY" // environment variable name for private key - rpcUrlEnvVarName = "SEQUENCER_RPC_URL" // environment variable name for RPC URL - anvilPortEnvVarName = "ANVIL_PORT_RPC_HTTP" // environment variable name for Anvil port - orgRegistryEnvVarName = "SEQUENCER_ORGANIZATION_REGISTRY" // environment variable name for organization registry - processRegistryEnvVarName = "SEQUENCER_PROCESS_REGISTRY" // environment variable name for process registry - resultsVerifierEnvVarName = "SEQUENCER_RESULTS_ZK_VERIFIER" // environment variable name for results zk verifier - stateTransitionVerifierEnvVarName = "SEQUENCER_STATE_TRANSITION_ZK_VERIFIER" // environment variable name for state transition zk verifier - cspCensusEnvVarName = "CSP_CENSUS" // environment variable name to select between csp or merkle tree census (by default merkle tree) -) - -var ( - defaultBatchTimeWindow = 45 * time.Second // default batch time window for sequencer - defaultAPIPort = util.RandomInt(40000, 60000) - defaultCensus3Port = util.RandomInt(40000, 60000) - defaultCensus3URL = fmt.Sprintf("http://localhost:%d", defaultCensus3Port) -) - -// Services struct holds all test services -type Services struct { - API *service.APIService - Census3 *c3service.Service - Sequencer *sequencer.Sequencer - CensusDownloader *service.CensusDownloader - Storage *storage.Storage - Contracts *web3.Contracts -} - -func isDebugTest() bool { - return os.Getenv("DEBUG") != "" && os.Getenv("DEBUG") != "false" -} - -func testTimeoutChan(t *testing.T) <-chan time.Time { - // Set up timeout based on context deadline - var timeoutCh <-chan time.Time - deadline, hasDeadline := t.Deadline() - - if hasDeadline { - // If context has a deadline, set timeout to 15 seconds before it - // to allow for clean shutdown and error reporting - remainingTime := time.Until(deadline) - timeoutBuffer := 15 * time.Second - - // If we have less than the buffer time left, use half of the remaining time - if remainingTime <= timeoutBuffer { - timeoutBuffer = remainingTime / 2 - } - - effectiveTimeout := remainingTime - timeoutBuffer - timeoutCh = time.After(effectiveTimeout) - t.Logf("Test will timeout in %v (deadline: %v)", effectiveTimeout, deadline) - } else { - // No deadline set, use a reasonable default - timeOut := 20 * time.Minute - if isDebugTest() { - timeOut = 50 * time.Minute - } - timeoutCh = time.After(timeOut) - t.Logf("No test deadline found, using %s minute default timeout", timeOut.String()) - } - return timeoutCh -} - -func isCSPCensus() bool { - cspCensusEnvVar := os.Getenv(cspCensusEnvVarName) - return strings.ToLower(cspCensusEnvVar) == "true" || cspCensusEnvVar == "1" -} - -func testCensusOrigin() types.CensusOrigin { - if isCSPCensus() { - return types.CensusOriginCSPEdDSABN254V1 - } else { - return types.CensusOriginMerkleTreeOffchainDynamicV1 - } -} - -func testWrongCensusOrigin() types.CensusOrigin { - if isCSPCensus() { - return types.CensusOriginMerkleTreeOffchainStaticV1 - } else { - return types.CensusOriginCSPEdDSABN254V1 - } -} - -// setupAPI creates and starts a new API server for testing. -// It returns the server port. -func setupAPI( - ctx context.Context, - db *storage.Storage, - workerSeed string, - workerTokenExpiration time.Duration, - workerTimeout time.Duration, - banRules *workers.WorkerBanRules, - web3Conf config.DavinciWeb3Config, -) (*service.APIService, error) { - api := service.NewAPI(db, "127.0.0.1", defaultAPIPort, "test", web3Conf, false) - api.SetWorkerConfig(workerSeed, workerTokenExpiration, workerTimeout, banRules) - if err := api.Start(ctx); err != nil { - return nil, err - } - - // Wait for the HTTP server to start - time.Sleep(500 * time.Millisecond) - return api, nil -} - -// setupCensusService creates and starts a new census3-bigquery service for -// testing. -func setupCensusService() (*c3service.Service, func(), error) { - // create temp dir for census3-bigquery - tempDir, err := os.MkdirTemp("", "census3-bigquery-test-") - if err != nil { - return nil, nil, fmt.Errorf("failed to create temp dir for census3-bigquery: %w", err) - } - - srv, err := c3service.New(&c3config.Config{ - APIPort: defaultCensus3Port, - DataDir: tempDir, - MaxCensusSize: 1000000, - }) - if err != nil { - return nil, nil, fmt.Errorf("failed to create census3-bigquery service: %w", err) - } - - go func() { - if err := srv.Start(); err != nil { - log.Errorw(err, "census3-bigquery service exited with error") - } - }() - return srv, func() { - srv.Stop() - if err := os.RemoveAll(tempDir); err != nil { - log.Warnw("failed to remove census3-bigquery temp dir", "error", err) - } - }, nil -} - -// setupWeb3 sets up the web3 contracts for testing. It deploys the contracts -// if the environment variables are not set, if they are set it loads the -// contracts from the environment variables. It returns the contracts object -// and a cleanup function that should be called when done. -func setupWeb3(ctx context.Context) (*web3.Contracts, func(), error) { - // Get the environment variables - var ( - privKey = os.Getenv(privKeyEnvVarName) - rpcUrl = os.Getenv(rpcUrlEnvVarName) - orgRegistryAddr = os.Getenv(orgRegistryEnvVarName) - processRegistryAddr = os.Getenv(processRegistryEnvVarName) - stateTransitionZKVerifierAddr = os.Getenv(stateTransitionVerifierEnvVarName) - resultsZKVerifierAddr = os.Getenv(resultsVerifierEnvVarName) - ) - // Check if the environment variables are set to run the tests over local - // geth node or remote blockchain environment - localEnv := privKey == "" || rpcUrl == "" || orgRegistryAddr == "" || - processRegistryAddr == "" || resultsZKVerifierAddr == "" || stateTransitionZKVerifierAddr == "" - - // Store cleanup functions - var cleanupFuncs []func() - cleanup := func() { - // Execute cleanup functions in reverse order - for i := len(cleanupFuncs) - 1; i >= 0; i-- { - cleanupFuncs[i]() - } - } - - var deployerUrl string - if localEnv { - // Generate a random port for geth HTTP RPC - anvilPort := util.RandomInt(10000, 20000) - rpcUrl = fmt.Sprintf("http://localhost:%d", anvilPort) - // Set environment variables for docker-compose in the process environment - composeEnv := make(map[string]string) - composeEnv[anvilPortEnvVarName] = fmt.Sprintf("%d", anvilPort) - composeEnv[deployerServerPortEnvVarName] = fmt.Sprintf("%d", anvilPort+1) - composeEnv[privKeyEnvVarName] = testLocalAccountPrivKey - - // get branch and commit from the environment variables - if branchName := os.Getenv(contractsBranchNameEnvVarName); branchName != "" { - composeEnv[contractsBranchNameEnvVarName] = branchName - } - if commitHash := os.Getenv(contractsCommitHashEnvVarName); commitHash != "" { - composeEnv[contractsCommitHashEnvVarName] = commitHash - } else { - // get it from the go mod file - modData, err := os.ReadFile("../go.mod") - if err != nil { - return nil, nil, fmt.Errorf("failed to read go.mod file: %w", err) - } - modFile, err := modfile.Parse("go.mod", modData, nil) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse go.mod file: %w", err) - } - // get the commit hash from the replace directive - for _, r := range modFile.Require { - if r.Mod.Path != "github.com/vocdoni/davinci-contracts" { - continue - } - if versionParts := strings.Split(r.Mod.Version, "-"); len(versionParts) == 3 { - composeEnv[contractsCommitHashEnvVarName] = versionParts[2] - break - } - if versionParts := strings.Split(r.Mod.Version, "."); len(versionParts) == 3 { - composeEnv[contractsCommitHashEnvVarName] = r.Mod.Version - break - } - return nil, nil, fmt.Errorf("cannot parse davinci-contracts version: %s", r.Mod.Version) - - } - } - - log.Infow("deploying contracts in local environment", - "commit", composeEnv[contractsCommitHashEnvVarName], - "branch", composeEnv[contractsBranchNameEnvVarName]) - - // Create docker-compose instance - compose, err := tc.NewDockerCompose("docker/docker-compose.yml") - if err != nil { - return nil, nil, fmt.Errorf("failed to create docker compose: %w", err) - } - ctx2, cancel := context.WithCancel(ctx) - // Register cleanup for context cancellation - cleanupFuncs = append(cleanupFuncs, cancel) - - // Start docker-compose - log.Infow("starting Anvil docker compose", "gethPort", anvilPort) - err = compose.WithEnv(composeEnv).Up(ctx2, tc.Wait(true), tc.RemoveOrphans(true)) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to start docker compose: %w", err) - } - - // Register cleanup for docker compose shutdown - cleanupFuncs = append(cleanupFuncs, func() { - downCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) - defer cancel() - if downErr := compose.Down(downCtx, tc.RemoveOrphans(true), tc.RemoveVolumes(true)); downErr != nil { - log.Warnw("failed to stop docker compose", "error", downErr) - } - }) - - deployerCtx, cancel := context.WithTimeout(ctx, 1*time.Minute) - defer cancel() - // Get the enpoint of the deployer service - deployerContainer, err := compose.ServiceContainer(deployerCtx, "deployer") - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to get deployer container: %w", err) - } - deployerUrl, err = deployerContainer.Endpoint(deployerCtx, "http") - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to get deployer endpoint: %w", err) - } - } - - // Wait for the RPC to be ready - err := web3.WaitReadyRPC(ctx, rpcUrl) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to wait for RPC: %w", err) - } - - // Initialize the contracts object - contracts, err := web3.New([]string{rpcUrl}, "", 1.0) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to create web3 contracts: %w", err) - } - - // Define contracts addresses or deploy them - if localEnv { - type deployerResponse struct { - Txs []struct { - ContractName string `json:"contractName"` - ContractAddress string `json:"contractAddress"` - } `json:"transactions"` - } - - // Wait until contracts are deployed and get their addresses from - // deployer - contractsCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) - defer cancel() - var contractsAddresses *web3.Addresses - for contractsAddresses == nil { - select { - case <-contractsCtx.Done(): - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("timeout waiting for contracts to be deployed") - case <-time.After(5 * time.Second): - // Check if the contracts are deployed making an http request - // to /addresses.json - endpoint := fmt.Sprintf("%s/addresses.json", deployerUrl) - res, err := http.Get(endpoint) - if err != nil { - log.Infow("waiting for contracts to be deployed", - "error", err, - "deployUrl", endpoint) - continue - } - if res.StatusCode != http.StatusOK { - if err := res.Body.Close(); err != nil { - log.Warnw("failed to close deployer response body", "error", err) - } - log.Infow("waiting for contracts to be deployed", - "status", res.StatusCode, - "deployUrl", endpoint) - continue - } - // Decode the response - var deployerResp deployerResponse - err = json.NewDecoder(res.Body).Decode(&deployerResp) - if err := res.Body.Close(); err != nil { - log.Warnw("failed to close deployer response body", "error", err) - } - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to decode deployer response: %w", err) - } - contractsAddresses = new(web3.Addresses) - log.Infow("contracts addresses from deployer", - "logs", deployerResp.Txs) - for _, tx := range deployerResp.Txs { - switch tx.ContractName { - case "OrganizationRegistry": - contractsAddresses.OrganizationRegistry = common.HexToAddress(tx.ContractAddress) - case "ProcessRegistry": - contractsAddresses.ProcessRegistry = common.HexToAddress(tx.ContractAddress) - case "StateTransitionVerifierGroth16": - contractsAddresses.StateTransitionZKVerifier = common.HexToAddress(tx.ContractAddress) - case "ResultsVerifierGroth16": - contractsAddresses.ResultsZKVerifier = common.HexToAddress(tx.ContractAddress) - default: - log.Infow("unknown contract name", "name", tx.ContractName) - } - } - } - } - // Set the private key for the sequencer - err = contracts.SetAccountPrivateKey(util.TrimHex(testLocalAccountPrivKey)) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to set account private key: %w", err) - } - // Load the contracts addresses into the contracts object - err = contracts.LoadContracts(contractsAddresses) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to load contracts: %w", err) - } - log.Infow("contracts deployed and loaded", - "chainId", contracts.ChainID, - "addresses", contractsAddresses) - } else { - // Set the private key for the sequencer - err = contracts.SetAccountPrivateKey(util.TrimHex(privKey)) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to set account private key: %w", err) - } - // Create the contracts object with the addresses from the environment - err = contracts.LoadContracts(&web3.Addresses{ - OrganizationRegistry: common.HexToAddress(orgRegistryAddr), - ProcessRegistry: common.HexToAddress(processRegistryAddr), - ResultsZKVerifier: common.HexToAddress(resultsZKVerifierAddr), - StateTransitionZKVerifier: common.HexToAddress(stateTransitionZKVerifierAddr), - }) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to load contracts: %w", err) - } - } - - // Start the transaction manager - txm, err := txmanager.New(ctx, contracts.Web3Pool(), contracts.Client(), contracts.Signer(), txmanager.DefaultConfig(contracts.ChainID)) - if err != nil { - cleanup() // Clean up what we've done so far - return nil, nil, fmt.Errorf("failed to create transaction manager: %w", err) - } - txm.Start(ctx) - contracts.SetTxManager(txm) - cleanupFuncs = append(cleanupFuncs, func() { - txm.Stop() - }) - // Return the contracts object and cleanup function - return contracts, cleanup, nil -} - -// NewTestClient creates a new API client for testing. -func NewTestClient(port int) (*client.HTTPclient, error) { - return client.New(fmt.Sprintf("http://127.0.0.1:%d", port)) -} - -func NewTestService( - ctx context.Context, - tempDir string, - workerSecret string, - workerTokenExpiration time.Duration, - workerTimeout time.Duration, - banRules *workers.WorkerBanRules, -) (*Services, func(), error) { - // Initialize census3 service - c3srv, c3cleanup, err := setupCensusService() - if err != nil { - return nil, nil, fmt.Errorf("failed to setup census3 service: %w", err) - } - // Initialize the web3 contracts - contracts, web3Cleanup, err := setupWeb3(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to setup web3: %w", err) - } - - kv, err := metadb.New(db.TypePebble, tempDir) - if err != nil { - web3Cleanup() // Clean up web3 if db creation fails - return nil, nil, fmt.Errorf("failed to create database: %w", err) - } - stg := storage.New(kv) - - services := &Services{ - Census3: c3srv, - Storage: stg, - Contracts: contracts, - } - - // Start sequencer service - sequencer.AggregatorTickerInterval = time.Second * 2 - sequencer.NewProcessMonitorInterval = time.Second * 5 - vp := service.NewSequencer(stg, contracts, defaultBatchTimeWindow, nil) - seqCtx, seqCancel := context.WithCancel(ctx) - if err := vp.Start(seqCtx); err != nil { - seqCancel() - web3Cleanup() // Clean up web3 if sequencer fails to start - return nil, nil, fmt.Errorf("failed to start sequencer: %w", err) - } - services.Sequencer = vp.Sequencer - - if isDebugTest() { - logger.Set(zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"}).With().Timestamp().Logger()) - // Note: Debug prover is disabled when not in testing context - log.Info("Debug prover is disabled in non-testing context") - } - - // Start census downloader - cd := service.NewCensusDownloader(contracts, services.Storage, service.CensusDownloaderConfig{ - CleanUpInterval: time.Second * 5, - Expiration: time.Minute * 30, - Cooldown: time.Second * 10, - Attempts: 5, - }) - if err := cd.Start(ctx); err != nil { - vp.Stop() - seqCancel() - web3Cleanup() - return nil, nil, fmt.Errorf("failed to start census downloader: %w", err) - } - services.CensusDownloader = cd - - // Start StateSync - stateSync := service.NewStateSync(contracts, stg) - if err := stateSync.Start(ctx); err != nil { - cd.Stop() - vp.Stop() - seqCancel() - web3Cleanup() // Clean up web3 if process monitor fails to start - return nil, nil, fmt.Errorf("failed to start state sync: %v", err) - } - - // Start process monitor - pm := service.NewProcessMonitor(contracts, stg, cd, stateSync, time.Second*2) - if err := pm.Start(ctx); err != nil { - stateSync.Stop() - cd.Stop() - vp.Stop() - seqCancel() - web3Cleanup() // Clean up web3 if process monitor fails to start - return nil, nil, fmt.Errorf("failed to start process monitor: %w", err) - } - // Start API service - web3Conf := config.DavinciWeb3Config{ - ProcessRegistrySmartContract: contracts.ContractsAddresses.ProcessRegistry.String(), - OrganizationRegistrySmartContract: contracts.ContractsAddresses.OrganizationRegistry.String(), - ResultsZKVerifier: contracts.ContractsAddresses.ResultsZKVerifier.String(), - StateTransitionZKVerifier: contracts.ContractsAddresses.StateTransitionZKVerifier.String(), - } - api, err := setupAPI(ctx, stg, workerSecret, workerTokenExpiration, workerTimeout, banRules, web3Conf) - if err != nil { - pm.Stop() - stateSync.Stop() - cd.Stop() - vp.Stop() - seqCancel() - web3Cleanup() // Clean up web3 if API fails to start - return nil, nil, fmt.Errorf("failed to setup API: %w", err) - } - services.API = api - - // Create a combined cleanup function - cleanup := func() { - seqCancel() - api.Stop() - stateSync.Stop() - cd.Stop() - pm.Stop() - vp.Stop() - stg.Close() - c3cleanup() - web3Cleanup() - } - - return services, cleanup, nil -} - -func createCensusWithRandomVoters(ctx context.Context, origin types.CensusOrigin, nVoters int) ([]byte, string, []*ethereum.Signer, error) { - // Generate random participants - signers := []*ethereum.Signer{} - votes := []state.Vote{} - for range nVoters { - signer, err := ethereum.NewSigner() - if err != nil { - return nil, "", nil, fmt.Errorf("failed to generate signer: %w", err) - } - signers = append(signers, signer) - votes = append(votes, state.Vote{ - Address: signer.Address().Big(), - Weight: big.NewInt(testutil.Weight), - }) - privKey := signer.HexPrivateKey() - log.Infow("new voter created", - "address", signer.Address(), - "privKey", privKey.String()) - } - - if isCSPCensus() { - eddsaCSP, err := csp.New(types.CensusOriginCSPEdDSABN254V1, []byte(testLocalCSPSeed)) - if err != nil { - return nil, "", nil, fmt.Errorf("failed to create CSP: %w", err) - } - root := eddsaCSP.CensusRoot() - if root == nil { - return nil, "", nil, fmt.Errorf("census root is nil") - } - return root.Root, "http://myowncsp.test", signers, nil - } else { - censusRoot, censusURI, err := censustest.NewCensus3MerkleTreeForTest(ctx, origin, votes, defaultCensus3URL) - if err != nil { - return nil, "", nil, fmt.Errorf("failed to serve census merkle tree: %w", err) - } - return censusRoot.Bytes(), censusURI, signers, nil - } -} - -func createCensusWithVoters(ctx context.Context, origin types.CensusOrigin, signers ...*ethereum.Signer) ([]byte, string, []*ethereum.Signer, error) { - // Generate random participants - votes := []state.Vote{} - for _, signer := range signers { - votes = append(votes, state.Vote{ - Address: signer.Address().Big(), - Weight: big.NewInt(testutil.Weight), - }) - privKey := signer.HexPrivateKey() - log.Infow("new voter created", - "address", signer.Address(), - "privKey", privKey.String()) - } - - if isCSPCensus() { - eddsaCSP, err := csp.New(types.CensusOriginCSPEdDSABN254V1, []byte(testLocalCSPSeed)) - if err != nil { - return nil, "", nil, fmt.Errorf("failed to create CSP: %w", err) - } - root := eddsaCSP.CensusRoot() - if root == nil { - return nil, "", nil, fmt.Errorf("census root is nil") - } - return root.Root, "http://myowncsp.test", signers, nil - } else { - censusRoot, censusURI, err := censustest.NewCensus3MerkleTreeForTest(ctx, origin, votes, defaultCensus3URL) - if err != nil { - return nil, "", nil, fmt.Errorf("failed to serve census merkle tree: %w", err) - } - return censusRoot.Bytes(), censusURI, signers, nil - } -} - -func generateCensusProof(processID types.ProcessID, key []byte) (*types.CensusProof, error) { - if isCSPCensus() { - weight := new(types.BigInt).SetUint64(testutil.Weight) - eddsaCSP, err := csp.New(types.CensusOriginCSPEdDSABN254V1, []byte(testLocalCSPSeed)) - if err != nil { - return nil, fmt.Errorf("failed to create CSP: %w", err) - } - cspProof, err := eddsaCSP.GenerateProof(processID, common.BytesToAddress(key), weight) - if err != nil { - return nil, fmt.Errorf("failed to generate CSP proof: %w", err) - } - return cspProof, nil - } - return nil, nil -} - -func createOrganization(contracts *web3.Contracts) (common.Address, error) { - orgAddr := contracts.AccountAddress() - txHash, err := contracts.CreateOrganization(orgAddr, &types.OrganizationInfo{ - Name: fmt.Sprintf("Vocdoni test %x", orgAddr[:4]), - MetadataURI: "https://vocdoni.io", - }) - if err != nil { - return common.Address{}, fmt.Errorf("failed to create organization: %w", err) - } - - if err = contracts.WaitTxByHash(txHash, time.Second*30); err != nil { - return common.Address{}, fmt.Errorf("failed to wait for organization creation transaction: %w", err) - } - return orgAddr, nil -} - -func createProcessInSequencer( - contracts *web3.Contracts, - cli *client.HTTPclient, - censusOrigin types.CensusOrigin, - censusURI string, - censusRoot []byte, - ballotMode *types.BallotMode, -) (types.ProcessID, *types.EncryptionKey, *types.HexBytes, error) { - // Get the next process ID from the contracts - processID, err := contracts.NextProcessID(contracts.AccountAddress()) - if err != nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("failed to get next process ID: %w", err) - } - - // Sign the process creation request - signature, err := contracts.SignMessage(fmt.Appendf(nil, types.NewProcessMessageToSign, processID.String())) - if err != nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("failed to sign message: %w", err) - } - - process := &types.ProcessSetup{ - ProcessID: processID, - Census: &types.Census{ - CensusOrigin: censusOrigin, - CensusURI: censusURI, - CensusRoot: censusRoot, - }, - BallotMode: ballotMode, - Signature: signature, - } - - body, code, err := cli.Request(http.MethodPost, process, nil, api.ProcessesEndpoint) - if err != nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("failed to create process: %w", err) - } - if code != http.StatusOK { - return types.ProcessID{}, nil, nil, fmt.Errorf("unexpected status code creating process: %d, body: %s", code, string(body)) - } - - var resp types.ProcessSetupResponse - err = json.NewDecoder(bytes.NewReader(body)).Decode(&resp) - if err != nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("failed to decode process response: %w", err) - } - if resp.ProcessID == nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("process ID is nil") - } - if resp.EncryptionPubKey[0] == nil || resp.EncryptionPubKey[1] == nil { - return types.ProcessID{}, nil, nil, fmt.Errorf("encryption public key is nil") - } - - encryptionKeys := &types.EncryptionKey{ - X: resp.EncryptionPubKey[0], - Y: resp.EncryptionPubKey[1], - } - return processID, encryptionKeys, &resp.StateRoot, nil -} - -func createProcessInContracts( - contracts *web3.Contracts, - censusOrigin types.CensusOrigin, - censusURI string, - censusRoot []byte, - ballotMode *types.BallotMode, - encryptionKey *types.EncryptionKey, - stateRoot *types.HexBytes, - numVoters int, - duration ...time.Duration, -) (types.ProcessID, error) { - finalDuration := time.Hour - if len(duration) > 0 { - finalDuration = duration[0] - } - - pid, txHash, err := contracts.CreateProcess(&types.Process{ - Status: types.ProcessStatusReady, - OrganizationId: contracts.AccountAddress(), - EncryptionKey: encryptionKey, - StateRoot: stateRoot.BigInt(), - StartTime: time.Now().Add(1 * time.Minute), - Duration: finalDuration, - MaxVoters: types.NewInt(numVoters), - MetadataURI: "https://example.com/metadata", - BallotMode: ballotMode, - Census: &types.Census{ - CensusRoot: censusRoot, - CensusURI: censusURI, - CensusOrigin: censusOrigin, - }, - }) - if err != nil { - return types.ProcessID{}, fmt.Errorf("failed to create process: %w", err) - } - return pid, contracts.WaitTxByHash(*txHash, time.Second*15) -} - -func updateProcessCensusInContracts( - contracts *web3.Contracts, - pid types.ProcessID, - census types.Census, -) error { - txHash, err := contracts.SetProcessCensus(pid, census) - if err != nil { - return fmt.Errorf("failed to update process census: %w", err) - } - return contracts.WaitTxByHash(*txHash, time.Second*15) -} - -func createVote(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey, privKey *ethereum.Signer, k *big.Int, fields []*types.BigInt) (api.Vote, error) { - var err error - // emulate user inputs - address := ethcrypto.PubkeyToAddress(privKey.PublicKey) - if k == nil { - k, err = elgamal.RandK() - if err != nil { - return api.Vote{}, fmt.Errorf("failed to generate random k: %w", err) - } - } - // set voter weight - voterWeight := new(types.BigInt).SetInt(testutil.Weight) - // compose wasm inputs - wasmInputs := &ballotproof.BallotProofInputs{ - Address: address.Bytes(), - ProcessID: pid, - EncryptionKey: []*types.BigInt{encKey.X, encKey.Y}, - K: new(types.BigInt).SetBigInt(k), - BallotMode: bm, - Weight: voterWeight, - FieldValues: fields, - } - // generate the inputs for the ballot proof circuit - wasmResult, err := ballotproof.GenerateBallotProofInputs(wasmInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to generate ballot proof inputs: %w", err) - } - // encode the inputs to json - encodedCircomInputs, err := json.Marshal(wasmResult.CircomInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to marshal circom inputs: %w", err) - } - // generate the proof using the circom circuit - rawProof, pubInputs, err := ballotprooftest.CompileAndGenerateProofForTest(encodedCircomInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to compile and generate proof: %w", err) - } - // convert the proof to gnark format - circomProof, _, err := circomgnark.UnmarshalCircom(rawProof, pubInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to unmarshal circom proof: %w", err) - } - // sign the hash of the circuit inputs - signature, err := ballotprooftest.SignECDSAForTest(privKey, wasmResult.VoteID) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to sign ECDSA: %w", err) - } - // return the vote ready to be sent to the sequencer - return api.Vote{ - ProcessID: &wasmResult.ProcessID, - Address: wasmInputs.Address, - VoteID: wasmResult.VoteID, - Ballot: wasmResult.Ballot, - BallotProof: circomProof, - BallotInputsHash: wasmResult.BallotInputsHash, - Signature: signature.Bytes(), - }, nil -} - -func createVoteWithRandomFields(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey, privKey *ethereum.Signer, k *big.Int) (api.Vote, error) { - // generate random ballot fields - randFields := ballotprooftest.GenBallotFieldsForTest( - int(bm.NumFields), - int(bm.MaxValue.MathBigInt().Int64()), - int(bm.MinValue.MathBigInt().Int64()), - bm.UniqueValues) - // cast fields to types.BigInt - fields := []*types.BigInt{} - for _, f := range randFields { - fields = append(fields, (*types.BigInt)(f)) - } - return createVote(pid, bm, encKey, privKey, k, fields) -} - -func createVoteFromInvalidVoter(pid types.ProcessID, bm *types.BallotMode, encKey *types.EncryptionKey) (api.Vote, error) { - privKey, err := ethereum.NewSigner() - if err != nil { - return api.Vote{}, fmt.Errorf("failed to generate signer: %w", err) - } - // emulate user inputs - address := ethcrypto.PubkeyToAddress(privKey.PublicKey) - k, err := elgamal.RandK() - if err != nil { - return api.Vote{}, fmt.Errorf("failed to generate random k: %w", err) - } - // generate random ballot fields - randFields := ballotprooftest.GenBallotFieldsForTest( - int(bm.NumFields), - int(bm.MaxValue.MathBigInt().Int64()), - int(bm.MinValue.MathBigInt().Int64()), - bm.UniqueValues) - // compose wasm inputs - wasmInputs := &ballotproof.BallotProofInputs{ - Address: address.Bytes(), - ProcessID: pid, - EncryptionKey: []*types.BigInt{encKey.X, encKey.Y}, - K: new(types.BigInt).SetBigInt(k), - BallotMode: bm, - Weight: new(types.BigInt).SetInt(testutil.Weight), - FieldValues: randFields[:], - } - // generate the inputs for the ballot proof circuit - wasmResult, err := ballotproof.GenerateBallotProofInputs(wasmInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to generate ballot proof inputs: %w", err) - } - // encode the inputs to json - encodedCircomInputs, err := json.Marshal(wasmResult.CircomInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to marshal circom inputs: %w", err) - } - // generate the proof using the circom circuit - rawProof, pubInputs, err := ballotprooftest.CompileAndGenerateProofForTest(encodedCircomInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to compile and generate proof: %w", err) - } - // convert the proof to gnark format - circomProof, _, err := circomgnark.UnmarshalCircom(rawProof, pubInputs) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to unmarshal circom proof: %w", err) - } - // sign the hash of the circuit inputs - signature, err := ballotprooftest.SignECDSAForTest(privKey, wasmResult.VoteID) - if err != nil { - return api.Vote{}, fmt.Errorf("failed to sign ECDSA: %w", err) - } - // return the vote ready to be sent to the sequencer - return api.Vote{ - ProcessID: &wasmResult.ProcessID, - Address: wasmInputs.Address, - Ballot: wasmResult.Ballot, - BallotProof: circomProof, - BallotInputsHash: wasmResult.BallotInputsHash, - Signature: signature.Bytes(), - VoteID: wasmResult.VoteID, - CensusProof: types.CensusProof{ - Weight: new(types.BigInt).SetInt(testutil.Weight), - }, - }, nil -} - -func checkVoteStatus(cli *client.HTTPclient, pid types.ProcessID, voteIDs []types.HexBytes, expectedStatus string) (bool, []types.HexBytes, error) { - // Check vote status and return whether all votes have the expected status - allExpectedStatus := true - failed := []types.HexBytes{} - - // Check status for each vote - for _, voteID := range voteIDs { - // Construct the status endpoint URL - statusEndpoint := api.EndpointWithParam( - api.EndpointWithParam(api.VoteStatusEndpoint, - api.ProcessURLParam, pid.String()), - api.VoteIDURLParam, voteID.String()) - - // Make the request to get the vote status - body, statusCode, err := cli.Request("GET", nil, nil, statusEndpoint) - if err != nil { - return false, nil, fmt.Errorf("failed to request vote status: %w", err) - } - if statusCode != 200 { - return false, nil, fmt.Errorf("unexpected status code: %d", statusCode) - } - - // Parse the response body to get the status - var statusResponse api.VoteStatusResponse - err = json.NewDecoder(bytes.NewReader(body)).Decode(&statusResponse) - if err != nil { - return false, nil, fmt.Errorf("failed to decode status response: %w", err) - } - - // Verify the status is valid - if statusResponse.Status == "" { - return false, nil, fmt.Errorf("status is empty") - } - - // Check if the vote has the expected status - switch statusResponse.Status { - case storage.VoteIDStatusName(storage.VoteIDStatusError): - allExpectedStatus = allExpectedStatus && (expectedStatus == storage.VoteIDStatusName(storage.VoteIDStatusError)) - if expectedStatus != storage.VoteIDStatusName(storage.VoteIDStatusError) { - failed = append(failed, voteID) - } - case expectedStatus: - allExpectedStatus = allExpectedStatus && true - default: - allExpectedStatus = false - } - } - - return allExpectedStatus, failed, nil -} - -func updateMaxVoters( - contracts *web3.Contracts, - pid types.ProcessID, - numVoters int, -) error { - currentProcess, err := contracts.Process(pid) - if err != nil { - return fmt.Errorf("failed to get current process: %w", err) - } - currentMaxVoters := currentProcess.MaxVoters.MathBigInt().Int64() - if numVoters < int(currentMaxVoters) { - return fmt.Errorf("new max voters (%d) is less than current max voters (%d)", numVoters, currentMaxVoters) - } - txHash, err := contracts.SetProcessMaxVoters(pid, types.NewInt(numVoters)) - if err != nil { - return fmt.Errorf("failed to set process max voters: %w", err) - } - return contracts.WaitTxByHash(*txHash, time.Second*15) -} - -func votersCount(contracts *web3.Contracts, pid types.ProcessID) (int, error) { - process, err := contracts.Process(pid) - if err != nil { - return 0, fmt.Errorf("failed to get process: %w", err) - } - if process == nil || process.VotersCount == nil { - return 0, nil - } - return int(process.VotersCount.MathBigInt().Int64()), nil -} - -func overwrittenVotesCount(contracts *web3.Contracts, pid types.ProcessID) (int, error) { - process, err := contracts.Process(pid) - if err != nil { - return 0, fmt.Errorf("failed to get process: %w", err) - } - if process == nil || process.OverwrittenVotesCount == nil { - return 0, nil - } - return int(process.OverwrittenVotesCount.MathBigInt().Int64()), nil -} - -func finishProcessOnContract(contracts *web3.Contracts, pid types.ProcessID) error { - txHash, err := contracts.SetProcessStatus(pid, types.ProcessStatusEnded) - if err != nil { - return fmt.Errorf("failed to set process status: %w", err) - } - if txHash == nil { - return fmt.Errorf("transaction hash is nil") - } - if err = contracts.WaitTxByHash(*txHash, time.Second*30); err != nil { - return fmt.Errorf("failed to wait for transaction: %w", err) - } - return nil -} - -func publishedResults(contracts *web3.Contracts, pid types.ProcessID) ([]*types.BigInt, error) { - process, err := contracts.Process(pid) - if err != nil { - return nil, fmt.Errorf("failed to get process: %w", err) - } - if process == nil || process.Status != types.ProcessStatusResults || len(process.Result) == 0 { - return nil, nil - } - return process.Result, nil -} diff --git a/tests/integration_test.go b/tests/integration_test.go deleted file mode 100644 index dfb3696b..00000000 --- a/tests/integration_test.go +++ /dev/null @@ -1,403 +0,0 @@ -package tests - -import ( - "context" - "math/big" - "testing" - "time" - - qt "github.com/frankban/quicktest" - "github.com/vocdoni/davinci-node/api" - "github.com/vocdoni/davinci-node/crypto/elgamal" - "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" - "github.com/vocdoni/davinci-node/internal/testutil" - "github.com/vocdoni/davinci-node/log" - "github.com/vocdoni/davinci-node/prover/debug" - "github.com/vocdoni/davinci-node/storage" - "github.com/vocdoni/davinci-node/types" -) - -func TestIntegration(t *testing.T) { - // Install log monitor that panics on Error level logs - previousLogger := log.EnablePanicOnError(t.Name()) - defer log.RestoreLogger(previousLogger) - - numVoters := 2 - c := qt.New(t) - - // Setup - ctx := t.Context() - - censusCtx, cancel := context.WithCancel(ctx) - defer cancel() - - _, port := services.API.HostPort() - cli, err := NewTestClient(port) - c.Assert(err, qt.IsNil) - - var ( - pid types.ProcessID - stateRoot *types.HexBytes - encryptionKey *types.EncryptionKey - ballotMode *types.BallotMode - signers []*ethereum.Signer - censusRoot []byte - censusURI string - ) - - if isDebugTest() { - services.Sequencer.SetProver(debug.NewDebugProver(t)) - } - - c.Run("create process", func(c *qt.C) { - // Create census with numVoters participants - censusRoot, censusURI, signers, err = createCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, numVoters+1) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) - ballotMode = testutil.BallotModeInternal() - - if !isCSPCensus() { - // first try to reproduce some bugs we had in sequencer in the past - // but only if we are not using a CSP census - { - // create a different censusRoot for testing - root2, root2URI, _, err := createCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, numVoters*2) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) - // createProcessInSequencer should be idempotent, but there was - // a bug in this, test it's fixed - pid1, encryptionKey1, stateRoot1, err := createProcessInSequencer(services.Contracts, cli, testCensusOrigin(), root2URI, root2, ballotMode) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) - pid2, encryptionKey2, stateRoot2, err := createProcessInSequencer(services.Contracts, cli, testCensusOrigin(), root2URI, root2, ballotMode) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) - c.Assert(pid2.String(), qt.Equals, pid1.String()) - c.Assert(encryptionKey2, qt.DeepEquals, encryptionKey1) - c.Assert(stateRoot2.String(), qt.Equals, stateRoot1.String()) - // a subsequent call to create process, same processID but with - // different censusOrigin should return the same encryptionKey - // but yield a different stateRoot - pid3, encryptionKey3, stateRoot3, err := createProcessInSequencer(services.Contracts, cli, testWrongCensusOrigin(), root2URI, root2, ballotMode) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) - c.Assert(pid3.String(), qt.Equals, pid1.String()) - c.Assert(encryptionKey3, qt.DeepEquals, encryptionKey1) - c.Assert(stateRoot3.String(), qt.Not(qt.Equals), stateRoot1.String(), - qt.Commentf("sequencer is returning the same state root although process parameters changed")) - } - } - // this final call is the good one, with the real censusRoot, should - // return the correct stateRoot and encryptionKey that we'll use to - // create process in contracts - pid, encryptionKey, stateRoot, err = createProcessInSequencer(services.Contracts, cli, testCensusOrigin(), censusURI, censusRoot, ballotMode) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) - - // now create process in contracts - pid2, err := createProcessInContracts(services.Contracts, testCensusOrigin(), censusURI, censusRoot, ballotMode, encryptionKey, stateRoot, numVoters) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) - c.Assert(pid2.String(), qt.Equals, pid.String()) - - // create a timeout for the process creation, if it is greater than the - // test timeout use the test timeout - createProcessTimeout := time.Minute * 2 - if timeout, hasDeadline := t.Deadline(); hasDeadline { - remainingTime := time.Until(timeout) - if remainingTime < createProcessTimeout { - createProcessTimeout = remainingTime - } - } - // Wait for the process to be registered - createProcessCtx, cancel := context.WithTimeout(ctx, createProcessTimeout) - defer cancel() - - CreateProcessLoop: - for { - select { - case <-createProcessCtx.Done(): - c.Fatal("Timeout waiting for process to be created and registered") - c.FailNow() - default: - if _, err := services.Storage.Process(pid); err == nil { - break CreateProcessLoop - } - time.Sleep(time.Millisecond * 200) - } - } - t.Logf("Process ID: %s", pid.String()) - - // Wait for the process to be registered in the sequencer - for { - select { - case <-createProcessCtx.Done(): - c.Fatal("Timeout waiting for process to be registered in sequencer") - c.FailNow() - default: - if services.Sequencer.ExistsProcessID(pid) { - t.Logf("Process ID %s registered in sequencer", pid.String()) - return - } - time.Sleep(time.Millisecond * 200) - } - } - }) - - // Store the voteIDs returned from the API to check their status later - var voteIDs []types.HexBytes - var ks []*big.Int - - c.Run("create votes", func(c *qt.C) { - c.Assert(len(signers), qt.Equals, numVoters+1) - for i := range signers[:numVoters] { - // generate a vote for the first participant - k, err := elgamal.RandK() - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate random k for ballot %d", i)) - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signers[i], k) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signers[i].Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - _, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, 200) - - // Save the voteID for status checks - voteIDs = append(voteIDs, vote.VoteID) - ks = append(ks, k) - } - // Wait for the vote to be registered - t.Logf("Waiting for %d votes to be settled", numVoters) - }) - - c.Assert(ks, qt.HasLen, numVoters) - c.Assert(voteIDs, qt.HasLen, numVoters) - - c.Run("create invalid votes", func(c *qt.C) { - vote, err := createVoteFromInvalidVoter(pid, ballotMode, encryptionKey) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote from invalid voter")) - // Make the request to try cast the vote - body, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, api.ErrInvalidCensusProof.HTTPstatus) - c.Assert(string(body), qt.Contains, api.ErrInvalidCensusProof.Error()) - }) - - c.Run("try to overwrite valid votes", func(c *qt.C) { - for i := range signers[:numVoters] { - // generate a vote for the participant - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signers[i], ks[i]) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - // generate census proof for the participant - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signers[i].Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - body, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, api.ErrBallotAlreadyProcessing.HTTPstatus) - c.Assert(string(body), qt.Contains, api.ErrBallotAlreadyProcessing.Error()) - } - }) - - timeoutCh := testTimeoutChan(t) - - c.Run("wait for process votes", func(c *qt.C) { - // Create a ticker to check the status of votes every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - SettledVotesLoop: - for { - select { - case <-ticker.C: - // Check that votes are settled (state transitions confirmed on blockchain) - if allSettled, failed, err := checkVoteStatus(cli, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { - c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) - if len(failed) > 0 { - hexFailed := make([]string, len(failed)) - for i, v := range failed { - hexFailed[i] = v.String() - } - t.Fatalf("Some votes failed to be settled: %v", hexFailed) - } - } - votersCount, err := votersCount(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) - if votersCount < numVoters { - continue - } - break SettledVotesLoop - case <-timeoutCh: - c.Fatalf("Timeout waiting for votes to be settled and published at contract") - } - } - t.Log("All votes settled.") - }) - - c.Run("wait until the stateroot is updated", func(c *qt.C) { - // Create a ticker to check the state root every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - for { - select { - case <-ticker.C: - // Get the process from storage - process, err := services.Storage.Process(pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get process from storage")) - if process.StateRoot.String() == stateRoot.String() { - t.Log("Process state root not yet updated") - continue - } - t.Logf("Process state root updated, from %x to %x", stateRoot.Bytes(), process.StateRoot.Bytes()) - return - case <-timeoutCh: - c.Fatalf("Timeout waiting for process state root to be updated") - } - } - }) - - voteIDs = []types.HexBytes{} - c.Run("try to create a new vote even the maxVoters is reached", func(c *qt.C) { - extraSigner := signers[numVoters] // get an extra signer from the created census - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create new signer")) - // generate a vote for the new participant - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, extraSigner, nil) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - // generate census proof for the participant - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, extraSigner.Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - body, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, api.ErrProcessMaxVotersReached.HTTPstatus) - c.Assert(string(body), qt.Contains, api.ErrProcessMaxVotersReached.Error()) - - // Set the max voters to a higher number to allow new votes - err = updateMaxVoters(services.Contracts, pid, numVoters+1) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to update max voters")) - - // Wait 15 seconds for the process monitor to pick up the change - time.Sleep(15 * time.Second) - - // Make the request to cast the vote again - _, status, err = cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, 200) - - // append the new vote stuff to the lists for later checks - voteIDs = append(voteIDs, vote.VoteID) - }) - - c.Run("overwrite valid votes", func(c *qt.C) { - for i := range signers[:numVoters] { - // generate a vote for the participant - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signers[i], nil) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - // generate census proof for the participant - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signers[i].Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - _, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, 200) - c.Logf("Vote %d (addr: %s) created with ID: %s", i, vote.Address.String(), vote.VoteID.String()) - - // Save the voteID for status checks - voteIDs = append(voteIDs, vote.VoteID) - } - // Wait for the vote to be registered - t.Logf("Waiting for %d votes to be settled", len(voteIDs)) - }) - - c.Run("wait for process overwrite votes", func(c *qt.C) { - // Create a ticker to check the status of votes every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - ResultsLoop2: - for { - select { - case <-ticker.C: - // Check that votes are settled (state transitions confirmed on blockchain) - allSettled, failed, err := checkVoteStatus(cli, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to check overwrite vote status")) - if !allSettled { - if len(failed) > 0 { - hexFailed := make([]string, len(failed)) - for i, v := range failed { - hexFailed[i] = v.String() - } - t.Fatalf("Some overwrite votes failed to be processed: %v", hexFailed) - } - } - votersCount, err := votersCount(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) - overwrittenVotes, err := overwrittenVotesCount(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get count of overwritten votes from contract")) - if overwrittenVotes < numVoters || votersCount < numVoters+1 { - continue - } - break ResultsLoop2 - case <-timeoutCh: - c.Fatalf("Timeout waiting for overwrite votes to be processed and published at contract") - } - } - t.Log("All overwrite votes processed, finalizing process...") - }) - - c.Run("wait for publish votes", func(c *qt.C) { - err := finishProcessOnContract(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) - results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) - c.Assert(err, qt.IsNil) - c.Logf("Results calculated: %v, waiting for onchain results...", results) - - // Create a ticker to check the status of votes every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - results, err := publishedResults(services.Contracts, pid) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) - if results == nil { - t.Log("Results not yet published, waiting...") - continue - } - t.Logf("Results published: %v", results) - return - case <-timeoutCh: - c.Fatalf("Timeout waiting for votes to be processed and published at contract") - } - } - }) - - c.Run("try to send votes to ended process", func(c *qt.C) { - for i := range signers { - // generate a vote for the first participant - vote, err := createVoteWithRandomFields(pid, ballotMode, encryptionKey, signers[i], nil) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) - // generate census proof for the participant - if isCSPCensus() { - censusProof, err := generateCensusProof(pid, signers[i].Address().Bytes()) - c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) - c.Assert(censusProof, qt.Not(qt.IsNil)) - vote.CensusProof = *censusProof - } - // Make the request to cast the vote - body, status, err := cli.Request("POST", vote, nil, api.VotesEndpoint) - c.Assert(err, qt.IsNil) - c.Assert(status, qt.Equals, api.ErrProcessNotAcceptingVotes.HTTPstatus) - c.Assert(string(body), qt.Contains, api.ErrProcessNotAcceptingVotes.Error()) - } - }) -} diff --git a/tests/main_test.go b/tests/main_test.go index 22e8d780..7cd777bb 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -7,20 +7,17 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/vocdoni/davinci-node/internal/testutil" "github.com/vocdoni/davinci-node/log" "github.com/vocdoni/davinci-node/service" + "github.com/vocdoni/davinci-node/tests/helpers" "github.com/vocdoni/davinci-node/workers" ) -const ( - testWorkerSeed = "test-seed" - testWorkerTokenExpiration = 24 * time.Hour - testWorkerTimeout = time.Second * 5 -) - var ( - orgAddr common.Address - services *Services + orgAddr common.Address + services *helpers.TestServices + defaultBallotMode = testutil.BallotModeInternal() ) func TestMain(m *testing.M) { @@ -40,13 +37,17 @@ func TestMain(m *testing.M) { var err error var cleanup func() - services, cleanup, err = NewTestService(ctx, tempDir, testWorkerSeed, testWorkerTokenExpiration, testWorkerTimeout, workers.DefaultWorkerBanRules) + services, cleanup, err = helpers.NewTestServices(ctx, tempDir, + helpers.WorkerSeed, + helpers.WorkerTokenExpiration, + helpers.WorkerTimeout, + workers.DefaultWorkerBanRules) if err != nil { log.Fatalf("failed to setup test services: %v", err) } // create organization - if orgAddr, err = createOrganization(services.Contracts); err != nil { + if orgAddr, err = helpers.CreateOrganization(services.Contracts); err != nil { log.Fatalf("failed to create organization: %v", err) } log.Infof("Organization address: %s", orgAddr.String()) diff --git a/tests/max_voters_test.go b/tests/max_voters_test.go new file mode 100644 index 00000000..b84d28ab --- /dev/null +++ b/tests/max_voters_test.go @@ -0,0 +1,226 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestMaxVoters(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + initialVoters := 2 + totalVoters := initialVoters + 1 // one extra voter to test maxVoters limit + c := qt.New(t) + + var ( + err error + pid types.ProcessID + stateRoot *types.HexBytes + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create process", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, totalVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, totalVoters) + + // create process in the sequencer + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts with initialVoters as maxVoters + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, initialVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created in storage") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create votes", func(c *qt.C) { + for i, signer := range signers[:initialVoters] { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be settled", initialVoters) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == initialVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("wait until the stateroot is updated", func(c *qt.C) { + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Get the process from storage + process, err := services.Storage.Process(pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get process from storage")) + return process.StateRoot.String() != stateRoot.String() + }); err != nil { + c.Fatalf("Timeout waiting for process state root to be updated") + c.FailNow() + } + t.Logf("Process state root updated.") + }) + + c.Run("handle maxVoters reached", func(c *qt.C) { + voteIDs = []types.HexBytes{} // reset voteIDs slice to only store new vote + + extraSigner := signers[initialVoters] // get an extra signer from the created census + // generate a vote for the new participant + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, extraSigner, nil) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof for the participant + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, extraSigner.Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + + c.Run("try to create a new vote even the maxVoters is reached", func(c *qt.C) { + // Make the request to cast the vote + body, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, api.ErrProcessMaxVotersReached.HTTPstatus) + c.Assert(string(body), qt.Contains, api.ErrProcessMaxVotersReached.Error()) + }) + + c.Run("update maxVoters", func(c *qt.C) { + // Set the max voters to a higher number to allow new votes + err = helpers.UpdateMaxVotersOnChain(services.Contracts, pid, totalVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to update max voters")) + + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Get the process from storage + process, err := services.Storage.Process(pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get process from storage")) + return process.MaxVoters.MathBigInt().Int64() == int64(totalVoters) + }); err != nil { + c.Fatalf("Timeout waiting for process state root to be updated") + c.FailNow() + } + t.Logf("Process maxVoters updated.") + }) + + c.Run("update maxVoters and create a new vote", func(c *qt.C) { + // Make the request to cast the vote again + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // append the new vote stuff to the lists for later checks + voteIDs = append(voteIDs, vote.VoteID) + }) + }) + + c.Run("wait for settled extra votes", func(c *qt.C) { + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == totalVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All extra votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) +} diff --git a/tests/offchain_merkletree_dynamic_census_test.go b/tests/offchain_merkletree_dynamic_census_test.go new file mode 100644 index 00000000..76aba3ce --- /dev/null +++ b/tests/offchain_merkletree_dynamic_census_test.go @@ -0,0 +1,209 @@ +package tests + +import ( + "bytes" + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestOffChainMerkleTreeDynamicCensus(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + numVoters := 2 + c := qt.New(t) + + var ( + err error + pid types.ProcessID + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create process", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainDynamicV1, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, numVoters) + + // create process in sequencer + var stateRoot *types.HexBytes + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginMerkleTreeOffchainDynamicV1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginMerkleTreeOffchainDynamicV1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created and registered") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create votes", func(c *qt.C) { + for i, signer := range signers { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainDynamicV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + c.Assert(ks, qt.HasLen, numVoters) + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("test dynamic census", func(c *qt.C) { + // create a signer that is not in the census + signer, err := ethereum.NewSigner() + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create ethereum signer")) + // try to vote with the new signer, should fail + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainDynamicV1, pid, signer.Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + + c.Run("try to vote with a non-census voter", func(c *qt.C) { + // Make the request to cast the vote + body, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, api.ErrInvalidCensusProof.HTTPstatus) + c.Assert(string(body), qt.Contains, api.ErrInvalidCensusProof.Error()) + }) + + c.Run("update census", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // create a new census including the new signer + signers = append(signers, signer) + censusRoot, censusURI, _, err = helpers.NewCensusWithVoters(censusCtx, types.CensusOriginMerkleTreeOffchainDynamicV1, signers...) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + + // update the census in the contracts + err = helpers.UpdateCensusOnChain(services.Contracts, pid, types.Census{ + CensusOrigin: helpers.CurrentCensusOrigin(), + CensusRoot: censusRoot, + CensusURI: censusURI, + }) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to update process census in contracts")) + // wait to new census in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Second*10, func() bool { + process, err := services.Storage.Process(pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get process from storage")) + return bytes.Equal(process.Census.CensusRoot, censusRoot) + }); err != nil { + c.Fatal("Timeout waiting for process census to be updated in sequencer") + c.FailNow() + } + t.Log("Process census root updated") + }) + + c.Run("vote with the new census voter", func(c *qt.C) { + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + }) + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be registered and aggregated", numVoters) + if err := helpers.WaitUntilCondition(globalCtx, time.Second*5, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == len(voteIDs) + }); err != nil { + c.Fatalf("Timeout waiting for votes to be registered and aggregated") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, time.Second*10, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) +} diff --git a/tests/offchain_merkletree_static_census_test.go b/tests/offchain_merkletree_static_census_test.go new file mode 100644 index 00000000..f4cf87b7 --- /dev/null +++ b/tests/offchain_merkletree_static_census_test.go @@ -0,0 +1,147 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestOffChainMerkleTreeStaticCensus(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + numVoters := 2 + c := qt.New(t) + + var ( + err error + pid types.ProcessID + stateRoot *types.HexBytes + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create process", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, numVoters) + + // create process in the sequencer + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created in storage") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create votes", func(c *qt.C) { + for i, signer := range signers { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + c.Assert(ks, qt.HasLen, numVoters) + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be settled", numVoters) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == numVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) +} diff --git a/tests/overwrites_votes_test.go b/tests/overwrites_votes_test.go new file mode 100644 index 00000000..ef5acc56 --- /dev/null +++ b/tests/overwrites_votes_test.go @@ -0,0 +1,194 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestOverwriteVotes(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + numVoters := 2 + c := qt.New(t) + + var ( + err error + pid types.ProcessID + stateRoot *types.HexBytes + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create process", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, numVoters) + + // create process in the sequencer + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created in storage") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create votes", func(c *qt.C) { + for i := range signers { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signers[i], k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + c.Assert(ks, qt.HasLen, numVoters) + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be settled", numVoters) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == numVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("overwrite valid votes", func(c *qt.C) { + voteIDs = []types.HexBytes{} // reset voteIDs + + for i, signer := range signers { + // generate a vote for the participant + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, nil) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof for the participant + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + c.Logf("Vote %d (addr: %s) created with ID: %s", i, vote.Address.String(), vote.VoteID.String()) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + } + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("wait for settled overwrite votes", func(c *qt.C) { + t.Logf("Waiting for %d overwritten votes to be settled", len(voteIDs)) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check overwrite vote status")) + if !allSettled { + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + c.Fatalf("Some overwrite votes failed to be processed: %v", hexFailed) + c.FailNow() + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + overwrittenVotes, err := helpers.FetchProcessOnChainOverwrittenVotesCount(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get count of overwritten votes from contract")) + return overwrittenVotes == numVoters && votersCount == numVoters + }); err != nil { + c.Fatalf("Timeout waiting for overwrite votes to be settled and published at contract") + c.FailNow() + } + t.Log("All overwritten votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) +} diff --git a/tests/vote_casting_rejections_test.go b/tests/vote_casting_rejections_test.go new file mode 100644 index 00000000..b84148fd --- /dev/null +++ b/tests/vote_casting_rejections_test.go @@ -0,0 +1,211 @@ +package tests + +import ( + "context" + "math/big" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/vocdoni/davinci-node/api" + "github.com/vocdoni/davinci-node/crypto/elgamal" + "github.com/vocdoni/davinci-node/crypto/signatures/ethereum" + "github.com/vocdoni/davinci-node/log" + "github.com/vocdoni/davinci-node/prover/debug" + "github.com/vocdoni/davinci-node/storage" + "github.com/vocdoni/davinci-node/tests/helpers" + "github.com/vocdoni/davinci-node/types" +) + +func TestVoteCastingRejections(t *testing.T) { + // Install log monitor that panics on Error level logs + previousLogger := log.EnablePanicOnError(t.Name()) + defer log.RestoreLogger(previousLogger) + + // Create a global context to be used throughout the test + globalCtx, globalCancel := context.WithTimeout(t.Context(), helpers.MaxTestTimeout(t)) + defer globalCancel() + + numVoters := 2 + c := qt.New(t) + + var ( + err error + pid types.ProcessID + stateRoot *types.HexBytes + encryptionKey *types.EncryptionKey + signers []*ethereum.Signer + censusRoot []byte + censusURI string + // Store the voteIDs returned from the API to check their status later + voteIDs []types.HexBytes + ks []*big.Int + ) + + if helpers.IsDebugTest() { + services.Sequencer.SetProver(debug.NewDebugProver(t)) + } + + c.Run("create census", func(c *qt.C) { + censusCtx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Create census with numVoters participants + censusRoot, censusURI, signers, err = helpers.NewCensusWithRandomVoters(censusCtx, types.CensusOriginMerkleTreeOffchainStaticV1, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create census")) + c.Assert(len(signers), qt.Equals, numVoters) + }) + + c.Run("same state root with different process parameters", func(c *qt.C) { + // createProcessInSequencer should be idempotent, but there was + // a bug in this, test it's fixed + pid1, encryptionKey1, stateRoot1, err := helpers.NewProcess(services.Contracts, services.HTTPClient, helpers.CurrentCensusOrigin(), censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + pid2, encryptionKey2, stateRoot2, err := helpers.NewProcess(services.Contracts, services.HTTPClient, helpers.CurrentCensusOrigin(), censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + c.Assert(pid2.String(), qt.Equals, pid1.String()) + c.Assert(encryptionKey2, qt.DeepEquals, encryptionKey1) + c.Assert(stateRoot2.String(), qt.Equals, stateRoot1.String()) + // a subsequent call to create process, same processID but with + // different censusOrigin should return the same encryptionKey + // but yield a different stateRoot + pid3, encryptionKey3, stateRoot3, err := helpers.NewProcess(services.Contracts, services.HTTPClient, helpers.WrongCensusOrigin(), censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + c.Assert(pid3.String(), qt.Equals, pid1.String()) + c.Assert(encryptionKey3, qt.DeepEquals, encryptionKey1) + c.Assert(stateRoot3.String(), qt.Not(qt.Equals), stateRoot1.String(), qt.Commentf("sequencer is returning the same state root although process parameters changed")) + }) + + c.Run("create process", func(c *qt.C) { + // create process in the sequencer + pid, encryptionKey, stateRoot, err = helpers.NewProcess(services.Contracts, services.HTTPClient, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in sequencer")) + + // now create process in contracts + onchainPID, err := helpers.NewProcessOnChain(services.Contracts, types.CensusOriginMerkleTreeOffchainStaticV1, censusURI, censusRoot, defaultBallotMode, encryptionKey, stateRoot, numVoters) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create process in contracts")) + c.Assert(onchainPID.String(), qt.Equals, pid.String()) + + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + _, err := services.Storage.Process(pid) + return err == nil + }); err != nil { + c.Fatal("Timeout waiting for process to be created in storage") + c.FailNow() + } + t.Logf("Process ID: %s", pid.String()) + + // Wait for the process to be registered in the sequencer + if err := helpers.WaitUntilCondition(globalCtx, time.Millisecond*200, func() bool { + return services.Sequencer.ExistsProcessID(pid) + }); err != nil { + c.Fatal("Timeout waiting for process to be registered in sequencer") + c.FailNow() + } + }) + + c.Run("create invalid votes", func(c *qt.C) { + vote, err := helpers.NewVoteFromNonCensusVoter(pid, defaultBallotMode, encryptionKey) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote from invalid voter")) + // Make the request to try cast the vote + body, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, api.ErrInvalidCensusProof.HTTPstatus) + c.Assert(string(body), qt.Contains, api.ErrInvalidCensusProof.Error()) + }) + + c.Run("create votes", func(c *qt.C) { + for i, signer := range signers { + // generate a vote for the first participant + k, err := elgamal.RandK() + c.Assert(err, qt.IsNil) + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, k) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + _, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, 200) + + // Save the voteID for status checks + voteIDs = append(voteIDs, vote.VoteID) + ks = append(ks, k) + } + c.Assert(ks, qt.HasLen, numVoters) + c.Assert(voteIDs, qt.HasLen, numVoters) + }) + + c.Run("try to overwrite valid votes", func(c *qt.C) { + for i, signer := range signers { + // generate a vote for the participant + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signer, ks[i]) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof for the participant + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + body, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, api.ErrBallotAlreadyProcessing.HTTPstatus) + c.Assert(string(body), qt.Contains, api.ErrBallotAlreadyProcessing.Error()) + } + }) + + c.Run("wait for settled votes", func(c *qt.C) { + t.Logf("Waiting for %d votes to be settled", numVoters) + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + // Check that votes are settled (state transitions confirmed on blockchain) + if allSettled, failed, err := helpers.EnsureVotesStatus(services.HTTPClient, pid, voteIDs, storage.VoteIDStatusName(storage.VoteIDStatusSettled)); !allSettled { + c.Assert(err, qt.IsNil, qt.Commentf("Failed to check vote status")) + if len(failed) > 0 { + hexFailed := types.SliceOf(failed, func(v types.HexBytes) string { return v.String() }) + t.Fatalf("Some votes failed to be settled: %v", hexFailed) + } + } + votersCount, err := helpers.FetchProcessVotersCountOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published votes from contract")) + return votersCount == numVoters + }); err != nil { + c.Fatalf("Timeout waiting for votes to be settled and published at contract") + c.FailNow() + } + t.Log("All votes settled.") + }) + + c.Run("finish process and wait for results", func(c *qt.C) { + err := helpers.FinishProcessOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to finish process on contract")) + results, err := services.Sequencer.WaitUntilResults(t.Context(), pid) + c.Assert(err, qt.IsNil) + c.Logf("Results calculated: %v, waiting for onchain results...", results) + + var pubResults []*types.BigInt + if err := helpers.WaitUntilCondition(globalCtx, 10*time.Second, func() bool { + pubResults, err = helpers.FetchResultsOnChain(services.Contracts, pid) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to get published results from contract")) + return pubResults != nil + }); err != nil { + c.Fatalf("Timeout waiting for votes to be processed and published at contract") + c.FailNow() + } + t.Logf("Results published: %v", pubResults) + }) + + c.Run("try to send votes to ended process", func(c *qt.C) { + for i := range signers { + // generate a vote for the first participant + vote, err := helpers.NewVoteWithRandomFields(pid, defaultBallotMode, encryptionKey, signers[i], nil) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to create vote")) + // generate census proof for the participant + vote.CensusProof, err = helpers.CreateCensusProof(types.CensusOriginMerkleTreeOffchainStaticV1, pid, signers[i].Address().Bytes()) + c.Assert(err, qt.IsNil, qt.Commentf("Failed to generate census proof")) + // Make the request to cast the vote + body, status, err := services.HTTPClient.Request("POST", vote, nil, api.VotesEndpoint) + c.Assert(err, qt.IsNil) + c.Assert(status, qt.Equals, api.ErrProcessNotAcceptingVotes.HTTPstatus) + c.Assert(string(body), qt.Contains, api.ErrProcessNotAcceptingVotes.Error()) + } + }) +}