From c83caf249edf19951bd7890f9e623f58dfc38d0b Mon Sep 17 00:00:00 2001 From: p4u Date: Wed, 19 Jun 2024 15:00:40 +0200 Subject: [PATCH] vochain: add set_process_duration transaction With this transaction we can now modify the duration of an existing project (while in READY or PAUSE state). If interruptible=false the duration can only be extended. The cost of the transactions is computed dynamically by: max(nextPrice - previousPrice, txBaseCost) Signed-off-by: p4u --- test/api_test.go | 17 +-- test/testcommon/api.go | 3 +- vochain/apptest.go | 2 + vochain/apputils.go | 2 +- vochain/genesis/genesis.go | 57 +++++----- vochain/genesis/txcost.go | 3 + vochain/indexer/db/processes.sql.go | 7 +- vochain/indexer/indexer.go | 7 ++ vochain/indexer/process.go | 1 + vochain/indexer/queries/processes.sql | 3 +- vochain/keykeeper/keykeeper.go | 3 + .../offchaindatahandler.go | 1 + vochain/process_test.go | 106 ++++++++++++++++-- vochain/state/balances.go | 1 + vochain/state/eventlistener.go | 1 + vochain/state/process.go | 51 +++++++++ vochain/state/state_test.go | 1 + vochain/transaction/election_tx.go | 31 ++++- vochain/transaction/transaction.go | 16 ++- 19 files changed, 257 insertions(+), 56 deletions(-) diff --git a/test/api_test.go b/test/api_test.go index f28e70d1c..22350c0b0 100644 --- a/test/api_test.go +++ b/test/api_test.go @@ -21,6 +21,7 @@ import ( "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/util" "go.vocdoni.io/dvote/vochain" + "go.vocdoni.io/dvote/vochain/genesis" "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/dvote/vochain/processid" "go.vocdoni.io/dvote/vochain/state" @@ -189,9 +190,9 @@ func TestAPIaccount(t *testing.T) { err := json.Unmarshal(resp, &countAccts) qt.Assert(t, err, qt.IsNil) - // 2 accounts must exist: the previously new created account and the auxiliary + // 3 accounts must exist: the previously new created account plus burn + faucet // account used to transfer to the new account - qt.Assert(t, countAccts.Count, qt.Equals, uint64(2)) + qt.Assert(t, countAccts.Count, qt.Equals, uint64(3)) // get the accounts info resp, code = c.Request("GET", nil, "accounts", "page", "0") @@ -228,7 +229,7 @@ func TestAPIElectionCost(t *testing.T) { }, 10000, 5000, 5, 1000, - 6) + 2) // bigger census size, duration, reduced network capacity, etc runAPIElectionCostWithParams(t, @@ -241,7 +242,7 @@ func TestAPIElectionCost(t *testing.T) { }, 200000, 6000, 10, 100, - 762) + 753) // very expensive election runAPIElectionCostWithParams(t, @@ -254,7 +255,7 @@ func TestAPIElectionCost(t *testing.T) { }, 100000, 700000, 10, 100, - 547026) + 547017) } func TestAPIAccountTokentxs(t *testing.T) { @@ -382,7 +383,8 @@ func TestAPIAccountTokentxs(t *testing.T) { qt.Assert(t, gotAcct2.Address.String(), qt.Equals, hex.EncodeToString(signer2.Address().Bytes())) // compare the balance expected for the account 2 in the account list - qt.Assert(t, gotAcct2.Balance, qt.Equals, initBalance+amountAcc1toAcct2-amountAcc2toAcct1) + txBasePrice := genesis.DefaultTransactionCosts().SendTokens + qt.Assert(t, gotAcct2.Balance, qt.Equals, initBalance+amountAcc1toAcct2-amountAcc2toAcct1-uint64(txBasePrice)) gotAcct1 := accts.Accounts[2] @@ -390,7 +392,7 @@ func TestAPIAccountTokentxs(t *testing.T) { qt.Assert(t, gotAcct1.Address.String(), qt.Equals, hex.EncodeToString(signer.Address().Bytes())) // compare the balance expected for the account 1 in the account list - qt.Assert(t, gotAcct1.Balance, qt.Equals, initBalance+amountAcc2toAcct1-amountAcc1toAcct2) + qt.Assert(t, gotAcct1.Balance, qt.Equals, initBalance+amountAcc2toAcct1-amountAcc1toAcct2-uint64(txBasePrice)) } @@ -418,6 +420,7 @@ func runAPIElectionCostWithParams(t *testing.T, err = server.VochainAPP.State.SetElectionPriceCalc() qt.Assert(t, err, qt.IsNil) server.VochainAPP.State.ElectionPriceCalc.SetCapacity(networkCapacity) + server.VochainAPP.State.ElectionPriceCalc.SetBasePrice(1) // Block 1 server.VochainAPP.AdvanceTestBlock() diff --git a/test/testcommon/api.go b/test/testcommon/api.go index 47ef9ff98..2057add3f 100644 --- a/test/testcommon/api.go +++ b/test/testcommon/api.go @@ -65,7 +65,8 @@ func (d *APIserver) Start(t testing.TB, apis ...string) { // create and add balance for the pre-created Account err = d.VochainAPP.State.CreateAccount(d.Account.Address(), "", nil, 1000000) qt.Assert(t, err, qt.IsNil) - d.VochainAPP.CommitState() + _, err = d.VochainAPP.CommitState() + qt.Assert(t, err, qt.IsNil) // create vochain info (we do not start since it is not required) d.VochainInfo = vochaininfo.NewVochainInfo(d.VochainAPP) diff --git a/vochain/apptest.go b/vochain/apptest.go index facb0b321..81a74a702 100644 --- a/vochain/apptest.go +++ b/vochain/apptest.go @@ -49,6 +49,8 @@ func TestBaseApplicationWithChainID(tb testing.TB, chainID string) *BaseApplicat if err != nil { tb.Fatal(err) } + app.State.ElectionPriceCalc.SetBasePrice(1) + app.State.ElectionPriceCalc.SetCapacity(10000) // TODO: should this be a Close on the entire BaseApplication? tb.Cleanup(func() { if err := app.State.Close(); err != nil { diff --git a/vochain/apputils.go b/vochain/apputils.go index 7628fcf01..aedf5ba58 100644 --- a/vochain/apputils.go +++ b/vochain/apputils.go @@ -134,7 +134,7 @@ func NewTemplateGenesisFile(dir string, validators int) (*genesis.Doc, error) { Balance: 100000, }, }, - TxCost: genesis.TransactionCosts{}, + TxCost: genesis.DefaultTransactionCosts(), } appState.MaxElectionSize = 100000 diff --git a/vochain/genesis/genesis.go b/vochain/genesis/genesis.go index 0c52efb92..93c6d6a2c 100644 --- a/vochain/genesis/genesis.go +++ b/vochain/genesis/genesis.go @@ -179,22 +179,7 @@ var initialAppStateForTest = AppState{ Balance: 1000000000000, }, }, - TxCost: TransactionCosts{ - SetProcessStatus: 1, - SetProcessCensus: 1, - SetProcessQuestionIndex: 1, - RegisterKey: 1, - NewProcess: 10, - SendTokens: 2, - SetAccountInfoURI: 2, - CreateAccount: 2, - AddDelegateForAccount: 2, - DelDelegateForAccount: 2, - CollectFaucet: 1, - SetAccountSIK: 1, - DelAccountSIK: 1, - SetAccountValidator: 100, - }, + TxCost: DefaultTransactionCosts(), } var initialAppStateForDev = AppState{ @@ -244,22 +229,7 @@ var initialAppStateForDev = AppState{ Balance: 100000000, }, }, - TxCost: TransactionCosts{ - SetProcessStatus: 2, - SetProcessCensus: 2, - SetProcessQuestionIndex: 1, - RegisterKey: 1, - NewProcess: 5, - SendTokens: 1, - SetAccountInfoURI: 1, - CreateAccount: 1, - AddDelegateForAccount: 1, - DelDelegateForAccount: 1, - CollectFaucet: 1, - SetAccountSIK: 1, - DelAccountSIK: 1, - SetAccountValidator: 10000, - }, + TxCost: DefaultTransactionCosts(), } var initialAppStateForStage = AppState{ @@ -321,6 +291,7 @@ var initialAppStateForStage = AppState{ TxCost: TransactionCosts{ SetProcessStatus: 2, SetProcessCensus: 1, + SetProcessDuration: 2, SetProcessQuestionIndex: 1, RegisterKey: 1, NewProcess: 5, @@ -412,6 +383,7 @@ var initialAppStateForLTS = AppState{ TxCost: TransactionCosts{ SetProcessStatus: 1, SetProcessCensus: 5, + SetProcessDuration: 5, SetProcessQuestionIndex: 1, RegisterKey: 1, NewProcess: 10, @@ -462,6 +434,27 @@ func DefaultValidatorParams() comettypes.ValidatorParams { } } +// DefaultTransactionCosts returns a default set of transaction costs to use as template. +func DefaultTransactionCosts() TransactionCosts { + return TransactionCosts{ + SetProcessStatus: 2, + SetProcessCensus: 2, + SetProcessDuration: 2, + SetProcessQuestionIndex: 1, + RegisterKey: 1, + NewProcess: 5, + SendTokens: 1, + SetAccountInfoURI: 1, + CreateAccount: 1, + AddDelegateForAccount: 1, + DelDelegateForAccount: 1, + CollectFaucet: 1, + SetAccountSIK: 1, + DelAccountSIK: 1, + SetAccountValidator: 10000, + } +} + // AvailableNetworks returns the list of hardcoded networks func AvailableNetworks() []string { list := []string{} diff --git a/vochain/genesis/txcost.go b/vochain/genesis/txcost.go index 577bae1a9..76c63da13 100644 --- a/vochain/genesis/txcost.go +++ b/vochain/genesis/txcost.go @@ -10,6 +10,7 @@ import ( type TransactionCosts struct { SetProcessStatus uint32 `json:"Tx_SetProcessStatus"` SetProcessCensus uint32 `json:"Tx_SetProcessCensus"` + SetProcessDuration uint32 `json:"Tx_SetProcessDuration"` SetProcessQuestionIndex uint32 `json:"Tx_SetProcessQuestionIndex"` RegisterKey uint32 `json:"Tx_RegisterKey"` NewProcess uint32 `json:"Tx_NewProcess"` @@ -43,6 +44,7 @@ func (t *TransactionCosts) AsMap() map[models.TxType]uint64 { var TxCostNameToTxTypeMap = map[string]models.TxType{ "SetProcessStatus": models.TxType_SET_PROCESS_STATUS, "SetProcessCensus": models.TxType_SET_PROCESS_CENSUS, + "SetProcessDuration": models.TxType_SET_PROCESS_DURATION, "SetProcessQuestionIndex": models.TxType_SET_PROCESS_QUESTION_INDEX, "SendTokens": models.TxType_SEND_TOKENS, "SetAccountInfoURI": models.TxType_SET_ACCOUNT_INFO_URI, @@ -69,6 +71,7 @@ func TxCostNameToTxType(key string) models.TxType { var TxTypeToCostNameMap = map[models.TxType]string{ models.TxType_SET_PROCESS_STATUS: "SetProcessStatus", models.TxType_SET_PROCESS_CENSUS: "SetProcessCensus", + models.TxType_SET_PROCESS_DURATION: "SetProcessDuration", models.TxType_SET_PROCESS_QUESTION_INDEX: "SetProcessQuestionIndex", models.TxType_SEND_TOKENS: "SendTokens", models.TxType_SET_ACCOUNT_INFO_URI: "SetAccountInfoURI", diff --git a/vochain/indexer/db/processes.sql.go b/vochain/indexer/db/processes.sql.go index 6b313659c..73220d6b3 100644 --- a/vochain/indexer/db/processes.sql.go +++ b/vochain/indexer/db/processes.sql.go @@ -391,8 +391,9 @@ SET census_root = ?1, public_keys = ?4, metadata = ?5, status = ?6, - max_census_size = ?7 -WHERE id = ?8 + max_census_size = ?7, + end_date = ?8 +WHERE id = ?9 ` type UpdateProcessFromStateParams struct { @@ -403,6 +404,7 @@ type UpdateProcessFromStateParams struct { Metadata string Status int64 MaxCensusSize int64 + EndDate time.Time ID types.ProcessID } @@ -415,6 +417,7 @@ func (q *Queries) UpdateProcessFromState(ctx context.Context, arg UpdateProcessF arg.Metadata, arg.Status, arg.MaxCensusSize, + arg.EndDate, arg.ID, ) } diff --git a/vochain/indexer/indexer.go b/vochain/indexer/indexer.go index 562609cec..b72de2f03 100644 --- a/vochain/indexer/indexer.go +++ b/vochain/indexer/indexer.go @@ -631,6 +631,13 @@ func (idx *Indexer) OnProcessStatusChange(pid []byte, _ models.ProcessStatus, _ idx.blockUpdateProcs[string(pid)] = true } +// OnProcessDurationChange adds the process to blockUpdateProcs and, if ended, the resultsPool +func (idx *Indexer) OnProcessDurationChange(pid []byte, _ uint32, _ int32) { + idx.blockMu.Lock() + defer idx.blockMu.Unlock() + idx.blockUpdateProcs[string(pid)] = true +} + // OnRevealKeys checks if all keys have been revealed and in such case add the // process to the results queue func (idx *Indexer) OnRevealKeys(pid []byte, _ string, _ int32) { diff --git a/vochain/indexer/process.go b/vochain/indexer/process.go index a20b0aa64..80d247487 100644 --- a/vochain/indexer/process.go +++ b/vochain/indexer/process.go @@ -209,6 +209,7 @@ func (idx *Indexer) updateProcess(ctx context.Context, queries *indexerdb.Querie Metadata: p.GetMetadata(), Status: int64(p.Status), MaxCensusSize: int64(p.GetMaxCensusSize()), + EndDate: time.Unix(int64(p.StartTime+p.Duration), 0), }); err != nil { return err } diff --git a/vochain/indexer/queries/processes.sql b/vochain/indexer/queries/processes.sql index 8c62c1045..919eeccab 100644 --- a/vochain/indexer/queries/processes.sql +++ b/vochain/indexer/queries/processes.sql @@ -52,7 +52,8 @@ SET census_root = sqlc.arg(census_root), public_keys = sqlc.arg(public_keys), metadata = sqlc.arg(metadata), status = sqlc.arg(status), - max_census_size = sqlc.arg(max_census_size) + max_census_size = sqlc.arg(max_census_size), + end_date = sqlc.arg(end_date) WHERE id = sqlc.arg(id); -- name: GetProcessStatus :one diff --git a/vochain/keykeeper/keykeeper.go b/vochain/keykeeper/keykeeper.go index f9d07eb72..7f46f654d 100644 --- a/vochain/keykeeper/keykeeper.go +++ b/vochain/keykeeper/keykeeper.go @@ -276,3 +276,6 @@ func (*KeyKeeper) OnCensusUpdate(_, _ []byte, _ string, _ uint64) {} // OnCancel does nothing func (k *KeyKeeper) OnCancel(_ []byte, _ int32) {} + +// OnProcessDurationChange does nothing +func (k *KeyKeeper) OnProcessDurationChange(_ []byte, _ uint32, _ int32) {} diff --git a/vochain/offchaindatahandler/offchaindatahandler.go b/vochain/offchaindatahandler/offchaindatahandler.go index c58480aa7..aece2907a 100644 --- a/vochain/offchaindatahandler/offchaindatahandler.go +++ b/vochain/offchaindatahandler/offchaindatahandler.go @@ -173,3 +173,4 @@ func (*OffChainDataHandler) OnProcessStatusChange(_ []byte, _ models.ProcessStat func (*OffChainDataHandler) OnTransferTokens(_ *vochaintx.TokenTransfer) {} func (*OffChainDataHandler) OnProcessResults(_ []byte, _ *models.ProcessResult, _ int32) {} func (*OffChainDataHandler) OnSpendTokens(_ []byte, _ models.TxType, _ uint64, _ string) {} +func (*OffChainDataHandler) OnProcessDurationChange(_ []byte, _ uint32, _ int32) {} diff --git a/vochain/process_test.go b/vochain/process_test.go index 357a34692..4dfac454a 100644 --- a/vochain/process_test.go +++ b/vochain/process_test.go @@ -351,6 +351,33 @@ func testSetProcessCensus(t *testing.T, pid []byte, txSender *ethereum.SignKeys, return err } +func testSetProcessDuration(t *testing.T, pid []byte, txSender *ethereum.SignKeys, + app *BaseApplication, duration uint32) error { + var stx models.SignedTx + var err error + + txSenderAcc, err := app.State.GetAccount(txSender.Address(), false) + if err != nil { + return fmt.Errorf("cannot get tx sender account %s with error %w", txSender.Address(), err) + } + + tx := &models.SetProcessTx{ + Txtype: models.TxType_SET_PROCESS_DURATION, + Nonce: txSenderAcc.Nonce, + ProcessId: pid, + Duration: &duration, + } + if stx.Tx, err = proto.Marshal(&models.Tx{Payload: &models.Tx_SetProcess{SetProcess: tx}}); err != nil { + return fmt.Errorf("cannot mashal tx %w", err) + } + if stx.Signature, err = txSender.SignVocdoniTx(stx.Tx, app.chainID); err != nil { + return fmt.Errorf("cannot sign tx %+v with error %w", tx, err) + } + + _, err = testCheckTxDeliverTxCommit(t, app, &stx) + return err +} + func TestCount(t *testing.T) { app := TestBaseApplication(t) count, err := app.State.CountProcesses(false) @@ -403,7 +430,10 @@ func createTestBaseApplicationAndAccounts(t *testing.T, qt.Assert(t, app.State.SetTxBaseCost(cost, txCostNumber), qt.IsNil) } + app.State.ElectionPriceCalc.SetBasePrice(10) + app.State.ElectionPriceCalc.SetCapacity(2000) testCommitState(t, app) + return app, keys } @@ -472,7 +502,7 @@ func testCheckTxDeliverTxCommit(t *testing.T, app *BaseApplication, stx *models. func TestGlobalMaxProcessSize(t *testing.T) { app, accounts := createTestBaseApplicationAndAccounts(t, 10) - app.State.SetMaxProcessSize(10) + qt.Assert(t, app.State.SetMaxProcessSize(10), qt.IsNil) app.AdvanceTestBlock() // define process @@ -487,22 +517,20 @@ func TestGlobalMaxProcessSize(t *testing.T) { CensusRoot: util.RandomBytes(32), CensusURI: &censusURI, CensusOrigin: models.CensusOrigin_OFF_CHAIN_TREE, - BlockCount: 1024, - MaxCensusSize: 20, + Duration: 60, + MaxCensusSize: 5, } - // create process with entityID (should fail) - qt.Assert(t, testCreateProcess(t, accounts[0], app, process), qt.IsNil) + // create process with maxcensussize < 10 (should work) + qt.Assert(t, testCreateProcessWithErr(t, accounts[0], app, process), qt.IsNil) - // create process with entityID (should work) - process.MaxCensusSize = 5 - qt.Assert(t, testCreateProcess(t, accounts[0], app, process), qt.IsNotNil) + // create process with maxcensussize > 10 (should fail) + process.MaxCensusSize = 20 + qt.Assert(t, testCreateProcessWithErr(t, accounts[0], app, process), qt.IsNotNil) } func TestSetProcessCensusSize(t *testing.T) { app, accounts := createTestBaseApplicationAndAccounts(t, 10) - app.State.ElectionPriceCalc.SetBasePrice(10) - app.State.ElectionPriceCalc.SetCapacity(2000) // define process censusURI := ipfsUrlTest @@ -571,3 +599,61 @@ func TestSetProcessCensusSize(t *testing.T) { // check that newBalance is at least 100 tokens less than oldBalance qt.Assert(t, oldBalance-newBalance >= 100, qt.IsTrue) } + +func TestSetProcessDuration(t *testing.T) { + app, accounts := createTestBaseApplicationAndAccounts(t, 10) + + // define process + censusURI := ipfsUrlTest + process := &models.Process{ + StartBlock: 1, + EnvelopeType: &models.EnvelopeType{EncryptedVotes: false}, + Mode: &models.ProcessMode{Interruptible: true, DynamicCensus: false}, + VoteOptions: &models.ProcessVoteOptions{MaxCount: 16, MaxValue: 16}, + Status: models.ProcessStatus_READY, + EntityId: accounts[0].Address().Bytes(), + CensusRoot: util.RandomBytes(32), + CensusURI: &censusURI, + CensusOrigin: models.CensusOrigin_OFF_CHAIN_TREE, + Duration: 60, + MaxCensusSize: 2, + } + + // create the process + pid := testCreateProcess(t, accounts[0], app, process) + app.AdvanceTestBlock() + + proc, err := app.State.Process(pid, true) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, proc.Duration, qt.Equals, uint32(60)) + + // Set lower duration (should workd) + qt.Assert(t, testSetProcessDuration(t, pid, accounts[0], app, 50), qt.IsNil) + app.AdvanceTestBlock() + + proc, err = app.State.Process(pid, true) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, proc.Duration, qt.Equals, uint32(50)) + + // Set higher duration (should work) + qt.Assert(t, testSetProcessDuration(t, pid, accounts[0], app, 80), qt.IsNil) + app.AdvanceTestBlock() + + proc, err = app.State.Process(pid, true) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, proc.Duration, qt.Equals, uint32(80)) + + // Check cost is increased with larger duration (should work) + account, err := app.State.GetAccount(accounts[0].Address(), true) + qt.Assert(t, err, qt.IsNil) + oldBalance := account.Balance + + qt.Assert(t, testSetProcessDuration(t, pid, accounts[0], app, 2000000), qt.IsNil) + + account, err = app.State.GetAccount(accounts[0].Address(), true) + qt.Assert(t, err, qt.IsNil) + newBalance := account.Balance + + // check that newBalance is at least 30 tokens less than oldBalance + qt.Assert(t, oldBalance-newBalance >= 30, qt.IsTrue) +} diff --git a/vochain/state/balances.go b/vochain/state/balances.go index 28c0b85b1..fac25df09 100644 --- a/vochain/state/balances.go +++ b/vochain/state/balances.go @@ -17,6 +17,7 @@ import ( var ( TxTypeCostToStateKey = map[models.TxType]string{ models.TxType_SET_PROCESS_STATUS: "c_setProcessStatus", + models.TxType_SET_PROCESS_DURATION: "c_setProcessDuration", models.TxType_SET_PROCESS_CENSUS: "c_setProcessCensus", models.TxType_SET_PROCESS_QUESTION_INDEX: "c_setProcessResults", models.TxType_REGISTER_VOTER_KEY: "c_registerKey", diff --git a/vochain/state/eventlistener.go b/vochain/state/eventlistener.go index 90748aa2a..1c2d05281 100644 --- a/vochain/state/eventlistener.go +++ b/vochain/state/eventlistener.go @@ -21,6 +21,7 @@ type EventListener interface { OnNewTx(tx *vochaintx.Tx, blockHeight uint32, txIndex int32) OnProcess(process *models.Process, txIndex int32) OnProcessStatusChange(pid []byte, status models.ProcessStatus, txIndex int32) + OnProcessDurationChange(pid []byte, newDuration uint32, txIndex int32) OnCancel(pid []byte, txIndex int32) OnProcessKeys(pid []byte, encryptionPub string, txIndex int32) OnRevealKeys(pid []byte, encryptionPriv string, txIndex int32) diff --git a/vochain/state/process.go b/vochain/state/process.go index efc299c79..19a7c049e 100644 --- a/vochain/state/process.go +++ b/vochain/state/process.go @@ -93,6 +93,9 @@ func getProcess(mainTreeView statedb.TreeViewer, pid []byte) (*models.Process, e if err != nil { return nil, fmt.Errorf("cannot unmarshal process (%s): %w", pid, err) } + if process.Process == nil { + return nil, fmt.Errorf("process %x is nil", pid) + } return process.Process, nil } @@ -270,6 +273,54 @@ func (v *State) SetProcessStatus(pid []byte, newstatus models.ProcessStatus, com return nil } +// SetProcessDuration sets the duration for a given process. +// If commit is true, the change is committed to the state and the event listeners are called. +// The new duration must be greater than zero and different from the current one. If the process is +// not interruptible, the new duration must be greater than the current one. +// The process must be in READY or PAUSED status. +func (v *State) SetProcessDuration(pid []byte, newDurationSeconds uint32, commit bool) error { + process, err := v.Process(pid, false) + if err != nil { + return err + } + currentTime, err := v.Timestamp(false) + if err != nil { + return fmt.Errorf("setProcessStatus: cannot get current timestamp: %w", err) + } + + if newDurationSeconds == 0 { + return fmt.Errorf("cannot set duration to zero") + } + + if newDurationSeconds == process.Duration { + return fmt.Errorf("cannot set duration to the same value") + } + + if process.Status != models.ProcessStatus_READY && process.Status != models.ProcessStatus_PAUSED { + return fmt.Errorf("cannot set duration, invalid status: %s", process.Status) + } + + if currentTime >= process.StartTime+newDurationSeconds { + return fmt.Errorf("cannot set duration to a value that has already passed") + } + + if !process.Mode.Interruptible && newDurationSeconds < process.Duration { + return fmt.Errorf("cannot shorten duration of non-interruptible process") + } + + // If all checks pass, the transition is valid + if commit { + process.Duration = newDurationSeconds + if err := v.UpdateProcess(process, process.ProcessId); err != nil { + return err + } + for _, l := range v.eventListeners { + l.OnProcessDurationChange(process.ProcessId, newDurationSeconds, v.txCounter.Load()) + } + } + return nil +} + // SetProcessResults sets the results for a given process and calls the event listeners. func (v *State) SetProcessResults(pid []byte, result *models.ProcessResult) error { process, err := v.Process(pid, false) diff --git a/vochain/state/state_test.go b/vochain/state/state_test.go index f32f26acb..26d692f56 100644 --- a/vochain/state/state_test.go +++ b/vochain/state/state_test.go @@ -186,6 +186,7 @@ func (*Listener) OnNewTx(_ *vochaintx.Tx, _ uint32, _ int32) func (*Listener) OnBeginBlock(BeginBlock) {} func (*Listener) OnProcess(_ *models.Process, _ int32) {} func (*Listener) OnProcessStatusChange(_ []byte, _ models.ProcessStatus, _ int32) {} +func (*Listener) OnProcessDurationChange(_ []byte, _ uint32, _ int32) {} func (*Listener) OnCancel(_ []byte, _ int32) {} func (*Listener) OnProcessKeys(_ []byte, _ string, _ int32) {} func (*Listener) OnRevealKeys(_ []byte, _ string, _ int32) {} diff --git a/vochain/transaction/election_tx.go b/vochain/transaction/election_tx.go index fa1ec472d..40df3defe 100644 --- a/vochain/transaction/election_tx.go +++ b/vochain/transaction/election_tx.go @@ -179,6 +179,9 @@ func (t *TransactionHandler) SetProcessTxCheck(vtx *vochaintx.Tx) (ethereum.Addr if err != nil { return ethereum.Address{}, err } + if addr == nil || acc == nil { + return ethereum.Address{}, fmt.Errorf("cannot get account from signature") + } // get process process, err := t.state.Process(tx.ProcessId, false) if err != nil { @@ -229,7 +232,7 @@ func (t *TransactionHandler) SetProcessTxCheck(vtx *vochaintx.Tx) (ethereum.Addr if err := t.checkMaxCensusSize(process); err != nil { return ethereum.Address{}, err } - // get Tx cost, since it is a new process, we should use the election price calculator + // get Tx cost, since it is a new census size, we should use the election price calculator if acc.Balance < t.txCostIncreaseCensusSize(process, tx.GetCensusSize()) { return ethereum.Address{}, fmt.Errorf("%w: required %d, got %d", vstate.ErrNotEnoughBalance, cost, acc.Balance) } @@ -241,6 +244,13 @@ func (t *TransactionHandler) SetProcessTxCheck(vtx *vochaintx.Tx) (ethereum.Addr tx.GetCensusSize(), false, ) + case models.TxType_SET_PROCESS_DURATION: + // get Tx cost, since it modifies the process duration, we should use the election price calculator + if acc.Balance < t.txCostIncreaseDuration(process, tx.GetDuration()) { + return ethereum.Address{}, fmt.Errorf("%w: required %d, got %d", vstate.ErrNotEnoughBalance, cost, acc.Balance) + } + return ethereum.Address(*addr), t.state.SetProcessDuration(process.ProcessId, tx.GetDuration(), false) + default: return ethereum.Address{}, fmt.Errorf("unknown setProcess tx type: %s", tx.Txtype) } @@ -353,3 +363,22 @@ func (t *TransactionHandler) txCostIncreaseCensusSize(process *models.Process, n } return baseCost + (newCost - oldCost) } + +// txCostIncreaseDuration calculates the cost increase of a process based on the new duration. +func (t *TransactionHandler) txCostIncreaseDuration(process *models.Process, newDuration uint32) uint64 { + oldCost := t.txElectionCostFromProcess(process) + oldDuration := process.GetDuration() + process.Duration = newDuration + newCost := t.txElectionCostFromProcess(process) + process.Duration = oldDuration + + baseCost, err := t.state.TxBaseCost(models.TxType_SET_PROCESS_DURATION, false) + if err != nil { + log.Errorw(err, "txCostIncreaseDuration: cannot get transaction base cost") + return 0 + } + if newCost < oldCost { + return baseCost + } + return baseCost + (newCost - oldCost) +} diff --git a/vochain/transaction/transaction.go b/vochain/transaction/transaction.go index b6f7d9743..474dac6c8 100644 --- a/vochain/transaction/transaction.go +++ b/vochain/transaction/transaction.go @@ -181,10 +181,24 @@ func (t *TransactionHandler) CheckTx(vtx *vochaintx.Tx, forCommit bool) (*Transa } cost = t.txCostIncreaseCensusSize(process, tx.GetCensusSize()) } - // update process census + // update process census on state if err := t.state.SetProcessCensus(tx.ProcessId, tx.CensusRoot, tx.GetCensusURI(), tx.GetCensusSize(), true); err != nil { return nil, fmt.Errorf("setProcessCensus: %s", err) } + case models.TxType_SET_PROCESS_DURATION: + if tx.GetDuration() == 0 { + return nil, fmt.Errorf("setProcessDuration: duration cannot be 0") + } + // if duration is increased, cost must be applied + process, err := t.state.Process(tx.ProcessId, false) + if err != nil { + return nil, fmt.Errorf("setProcessDuration: %s", err) + } + cost = t.txCostIncreaseDuration(process, tx.GetDuration()) + // update process duration on state + if err := t.state.SetProcessDuration(tx.ProcessId, tx.GetDuration(), true); err != nil { + return nil, fmt.Errorf("setProcessCensus: %s", err) + } default: return nil, fmt.Errorf("unknown set process tx type") }