diff --git a/cmd/daemon/init.go b/cmd/daemon/init.go index ad4f0e631..24634eab9 100644 --- a/cmd/daemon/init.go +++ b/cmd/daemon/init.go @@ -45,9 +45,11 @@ func buildInitCmd(parentCmd *cobra.Command) { return } + var mnemonic string if *restoreOpt == "" { mnemonic, _ = wallet.GenerateMnemonic(*entropyOpt) + cmd.PrintLine() cmd.PrintInfoMsgf("Your wallet seed is:") cmd.PrintInfoMsgBoldf(" " + mnemonic) diff --git a/execution/errors.go b/execution/errors.go index 9f435a7d6..79a5f850d 100644 --- a/execution/errors.go +++ b/execution/errors.go @@ -6,7 +6,6 @@ import ( "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/types/tx/payload" ) // TransactionCommittedError is returned when an attempt is made @@ -22,36 +21,25 @@ func (e TransactionCommittedError) Error() string { e.ID.String()) } -// UnknownPayloadTypeError is returned when transaction payload type -// is not valid. -type UnknownPayloadTypeError struct { - PayloadType payload.Type -} - -func (e UnknownPayloadTypeError) Error() string { - return fmt.Sprintf("unknown payload type: %s", - e.PayloadType.String()) -} - -// PastLockTimeError is returned when the lock time of a transaction +// LockTimeExpiredError is returned when the lock time of a transaction // is in the past and has expired, // indicating the transaction can no longer be executed. -type PastLockTimeError struct { +type LockTimeExpiredError struct { LockTime uint32 } -func (e PastLockTimeError) Error() string { - return fmt.Sprintf("lock time is in the past: %v", e.LockTime) +func (e LockTimeExpiredError) Error() string { + return fmt.Sprintf("lock time expired: %v", e.LockTime) } -// FutureLockTimeError is returned when the lock time of a transaction +// LockTimeInFutureError is returned when the lock time of a transaction // is in the future, // indicating the transaction is not yet eligible for processing. -type FutureLockTimeError struct { +type LockTimeInFutureError struct { LockTime uint32 } -func (e FutureLockTimeError) Error() string { +func (e LockTimeInFutureError) Error() string { return fmt.Sprintf("lock time is in the future: %v", e.LockTime) } diff --git a/execution/execution.go b/execution/execution.go index dd43d92ed..8ec1c326e 100644 --- a/execution/execution.go +++ b/execution/execution.go @@ -4,40 +4,26 @@ import ( "github.com/pactus-project/pactus/execution/executor" "github.com/pactus-project/pactus/sandbox" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/types/tx/payload" ) -type Executor interface { - Execute(trx *tx.Tx, sb sandbox.Sandbox) error -} -type Execution struct { - executors map[payload.Type]Executor - strict bool -} - -func newExecution(strict bool) *Execution { - execs := make(map[payload.Type]Executor) - execs[payload.TypeTransfer] = executor.NewTransferExecutor(strict) - execs[payload.TypeBond] = executor.NewBondExecutor(strict) - execs[payload.TypeSortition] = executor.NewSortitionExecutor(strict) - execs[payload.TypeUnbond] = executor.NewUnbondExecutor(strict) - execs[payload.TypeWithdraw] = executor.NewWithdrawExecutor(strict) - - return &Execution{ - executors: execs, - strict: strict, +func Execute(trx *tx.Tx, sb sandbox.Sandbox) error { + exe, err := executor.MakeExecutor(trx, sb) + if err != nil { + return err } -} -func NewExecutor() *Execution { - return newExecution(true) -} + exe.Execute() + sb.CommitTransaction(trx) -func NewChecker() *Execution { - return newExecution(false) + return nil } -func (exe *Execution) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func CheckAndExecute(trx *tx.Tx, sb sandbox.Sandbox, strict bool) error { + exe, err := executor.MakeExecutor(trx, sb) + if err != nil { + return err + } + if sb.IsBanned(trx.Payload().Signer()) { return SignerBannedError{ addr: trx.Payload().Signer(), @@ -50,31 +36,25 @@ func (exe *Execution) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { } } - if err := exe.checkLockTime(trx, sb); err != nil { + if err := CheckLockTime(trx, sb, strict); err != nil { return err } - if err := exe.checkFee(trx); err != nil { + if err := CheckFee(trx); err != nil { return err } - e, ok := exe.executors[trx.Payload().Type()] - if !ok { - return UnknownPayloadTypeError{ - PayloadType: trx.Payload().Type(), - } - } - - if err := e.Execute(trx, sb); err != nil { + if err := exe.Check(strict); err != nil { return err } + exe.Execute() sb.CommitTransaction(trx) return nil } -func (exe *Execution) checkLockTime(trx *tx.Tx, sb sandbox.Sandbox) error { +func CheckLockTime(trx *tx.Tx, sb sandbox.Sandbox, strict bool) error { interval := sb.Params().TransactionToLiveInterval if trx.IsSubsidyTx() { @@ -85,18 +65,18 @@ func (exe *Execution) checkLockTime(trx *tx.Tx, sb sandbox.Sandbox) error { if sb.CurrentHeight() > interval { if trx.LockTime() < sb.CurrentHeight()-interval { - return PastLockTimeError{ + return LockTimeExpiredError{ LockTime: trx.LockTime(), } } } - if exe.strict { + if strict { // In strict mode, transactions with future lock times are rejected. // In non-strict mode, they are added to the transaction pool and // processed once eligible. if trx.LockTime() > sb.CurrentHeight() { - return FutureLockTimeError{ + return LockTimeInFutureError{ LockTime: trx.LockTime(), } } @@ -105,7 +85,8 @@ func (exe *Execution) checkLockTime(trx *tx.Tx, sb sandbox.Sandbox) error { return nil } -func (*Execution) checkFee(trx *tx.Tx) error { +func CheckFee(trx *tx.Tx) error { + // TODO: This check maybe can be done in BasicCheck? if trx.IsFreeTx() { if trx.Fee() != 0 { return InvalidFeeError{ diff --git a/execution/execution_test.go b/execution/execution_test.go index 352683143..6bff7d6f9 100644 --- a/execution/execution_test.go +++ b/execution/execution_test.go @@ -4,195 +4,325 @@ import ( "testing" "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/execution/executor" "github.com/pactus-project/pactus/sandbox" + "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/util/errors" "github.com/pactus-project/pactus/util/testsuite" "github.com/stretchr/testify/assert" ) -func TestLockTime(t *testing.T) { +func TestTransferLockTime(t *testing.T) { ts := testsuite.NewTestSuite(t) sb := sandbox.MockingSandbox(ts) - sb.TestAcceptSortition = true - exe := NewExecutor() rndPubKey, rndPrvKey := ts.RandBLSKeyPair() rndAccAddr := rndPubKey.AccountAddress() rndAcc := sb.MakeNewAccount(rndAccAddr) - rndAcc.AddToBalance(100 * 1e9) + rndAcc.AddToBalance(1000 * 1e9) sb.UpdateAccount(rndAccAddr, rndAcc) + _ = sb.TestStore.AddTestBlock(8642) + + tests := []struct { + name string + lockTime uint32 + strictErr error + nonStrictErr error + }{ + { + name: "Transaction has expired LockTime (-8641)", + lockTime: sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval - 1, + strictErr: LockTimeExpiredError{sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval - 1}, + nonStrictErr: LockTimeExpiredError{sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval - 1}, + }, + { + name: "Transaction has valid LockTime (-8640)", + lockTime: sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval, + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Transaction has valid LockTime (-88)", + lockTime: sb.CurrentHeight() - 88, + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Transaction has valid LockTime (0)", + lockTime: sb.CurrentHeight(), + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Transaction has future LockTime (+1)", + lockTime: sb.CurrentHeight() + 1, + strictErr: LockTimeInFutureError{sb.CurrentHeight() + 1}, + nonStrictErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + trx := tx.NewTransferTx(tc.lockTime, rndAccAddr, ts.RandAccAddress(), 1000, 1000, "") + ts.HelperSignTransaction(rndPrvKey, trx) + + strictErr := CheckLockTime(trx, sb, true) + assert.ErrorIs(t, strictErr, tc.strictErr) + + nonStrictErr := CheckLockTime(trx, sb, false) + assert.ErrorIs(t, nonStrictErr, tc.nonStrictErr) + }) + } +} + +func TestSortitionLockTime(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + sb := sandbox.MockingSandbox(ts) + sb.TestAcceptSortition = true + rndPubKey, rndPrvKey := ts.RandBLSKeyPair() rndValAddr := rndPubKey.ValidatorAddress() rndVal := sb.MakeNewValidator(rndPubKey) - rndVal.AddToStake(100 * 1e9) + rndVal.AddToStake(1000 * 1e9) sb.UpdateValidator(rndVal) _ = sb.TestStore.AddTestBlock(8642) - t.Run("Future LockTime, Should return error (+1)", func(t *testing.T) { - lockTime := sb.CurrentHeight() + 1 - trx := tx.NewTransferTx(lockTime, rndAccAddr, ts.RandAccAddress(), 1000, 1000, "future-lockTime") - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, FutureLockTimeError{LockTime: lockTime}) - }) + tests := []struct { + name string + lockTime uint32 + strictErr error + nonStrictErr error + }{ + { + name: "Sortition transaction has expired LockTime (-8)", + lockTime: sb.CurrentHeight() - sb.TestParams.SortitionInterval - 1, + strictErr: LockTimeExpiredError{sb.CurrentHeight() - sb.TestParams.SortitionInterval - 1}, + nonStrictErr: LockTimeExpiredError{sb.CurrentHeight() - sb.TestParams.SortitionInterval - 1}, + }, + { + name: "Sortition transaction has valid LockTime (-7)", + lockTime: sb.CurrentHeight() - sb.TestParams.SortitionInterval, + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Sortition transaction has valid LockTime (-1)", + lockTime: sb.CurrentHeight() - 1, + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Sortition transaction has valid LockTime (0)", + lockTime: sb.CurrentHeight(), + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Sortition transaction has future LockTime (+1)", + lockTime: sb.CurrentHeight() + 1, + strictErr: LockTimeInFutureError{sb.CurrentHeight() + 1}, + nonStrictErr: nil, + }, + } - t.Run("Past LockTime, Should return error (-8641)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval - 1 - trx := tx.NewTransferTx(lockTime, rndAccAddr, ts.RandAccAddress(), 1000, 1000, "past-lockTime") - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, PastLockTimeError{LockTime: lockTime}) - }) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + trx := tx.NewSortitionTx(tc.lockTime, rndValAddr, ts.RandProof()) + ts.HelperSignTransaction(rndPrvKey, trx) - t.Run("Transaction has valid LockTime (-8640)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - sb.TestParams.TransactionToLiveInterval - trx := tx.NewTransferTx(lockTime, rndAccAddr, ts.RandAccAddress(), 1000, 1000, "ok") - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.NoError(t, err) - }) + strictErr := CheckLockTime(trx, sb, true) + assert.ErrorIs(t, strictErr, tc.strictErr) - t.Run("Transaction has valid LockTime (0)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - trx := tx.NewTransferTx(lockTime, rndAccAddr, ts.RandAccAddress(), 1000, 1000, "ok") - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.NoError(t, err) - }) + nonStrictErr := CheckLockTime(trx, sb, false) + assert.ErrorIs(t, nonStrictErr, tc.nonStrictErr) + }) + } +} - t.Run("Subsidy transaction has invalid LockTime (+1)", func(t *testing.T) { - lockTime := sb.CurrentHeight() + 1 - trx := tx.NewSubsidyTx(lockTime, ts.RandAccAddress(), 1000, - "invalid-lockTime") - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, FutureLockTimeError{LockTime: lockTime}) - }) +func TestSubsidyLockTime(t *testing.T) { + ts := testsuite.NewTestSuite(t) - t.Run("Subsidy transaction has invalid LockTime (-1)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - 1 - trx := tx.NewSubsidyTx(lockTime, ts.RandAccAddress(), 1000, - "invalid-lockTime") - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, PastLockTimeError{LockTime: lockTime}) - }) + sb := sandbox.MockingSandbox(ts) + _ = sb.TestStore.AddTestBlock(8642) - t.Run("Subsidy transaction has valid LockTime (0)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - trx := tx.NewSubsidyTx(lockTime, ts.RandAccAddress(), 1000, "ok") - err := exe.Execute(trx, sb) - assert.NoError(t, err) - }) + tests := []struct { + name string + lockTime uint32 + strictErr error + nonStrictErr error + }{ + { + name: "Subsidy transaction has expired LockTime (-1)", + lockTime: sb.CurrentHeight() - 1, + strictErr: LockTimeExpiredError{sb.CurrentHeight() - 1}, + nonStrictErr: LockTimeExpiredError{sb.CurrentHeight() - 1}, + }, + { + name: "Subsidy transaction has valid LockTime (0)", + lockTime: sb.CurrentHeight(), + strictErr: nil, + nonStrictErr: nil, + }, + { + name: "Subsidy transaction has future LockTime (+1)", + lockTime: sb.CurrentHeight() + 1, + strictErr: LockTimeInFutureError{sb.CurrentHeight() + 1}, + nonStrictErr: nil, + }, + } - t.Run("Sortition transaction has invalid LockTime (+1)", func(t *testing.T) { - lockTime := sb.CurrentHeight() + 1 - proof := ts.RandProof() - trx := tx.NewSortitionTx(lockTime, rndValAddr, proof) - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, FutureLockTimeError{LockTime: lockTime}) - }) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + trx := tx.NewSubsidyTx(tc.lockTime, ts.RandAccAddress(), 1000, "subsidy-test") - t.Run("Sortition transaction has invalid LockTime (-8)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - sb.TestParams.SortitionInterval - 1 - proof := ts.RandProof() - trx := tx.NewSortitionTx(lockTime, rndValAddr, proof) - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.ErrorIs(t, err, PastLockTimeError{LockTime: lockTime}) - }) + strictErr := CheckLockTime(trx, sb, true) + assert.ErrorIs(t, strictErr, tc.strictErr) - t.Run("Sortition transaction has valid LockTime (-7)", func(t *testing.T) { - lockTime := sb.CurrentHeight() - sb.TestParams.SortitionInterval - proof := ts.RandProof() + nonStrictErr := CheckLockTime(trx, sb, false) + assert.ErrorIs(t, nonStrictErr, tc.nonStrictErr) + }) + } +} - trx := tx.NewSortitionTx(lockTime, rndValAddr, proof) - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.NoError(t, err) - }) +func TestCheckFee(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + tests := []struct { + name string + trx *tx.Tx + expectedErr error + }{ + { + name: "Subsidy transaction with fee", + trx: tx.NewTransferTx(ts.RandHeight(), crypto.TreasuryAddress, ts.RandAccAddress(), + ts.RandAmount(), 1, ""), + expectedErr: InvalidFeeError{Fee: 1, Expected: 0}, + }, + { + name: "Subsidy transaction without fee", + trx: tx.NewTransferTx(ts.RandHeight(), crypto.TreasuryAddress, ts.RandAccAddress(), + ts.RandAmount(), 0, ""), + expectedErr: nil, + }, + { + name: "Transfer transaction with fee", + trx: tx.NewTransferTx(ts.RandHeight(), ts.RandAccAddress(), ts.RandAccAddress(), + ts.RandAmount(), 0, ""), + expectedErr: nil, + }, + { + name: "Transfer transaction without fee", + trx: tx.NewTransferTx(ts.RandHeight(), ts.RandAccAddress(), ts.RandAccAddress(), + ts.RandAmount(), 0, ""), + expectedErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := CheckFee(tc.trx) + assert.ErrorIs(t, err, tc.expectedErr) + }) + } } -func TestExecution(t *testing.T) { +func TestExecute(t *testing.T) { ts := testsuite.NewTestSuite(t) sb := sandbox.MockingSandbox(ts) - exe := NewExecutor() - - rndPubKey, rndPrvKey := ts.RandBLSKeyPair() - rndAccAddr := rndPubKey.AccountAddress() - rndValAddr := rndPubKey.ValidatorAddress() - rndAcc := sb.MakeNewAccount(rndAccAddr) - rndAcc.AddToBalance(100 * 1e9) - sb.UpdateAccount(rndAccAddr, rndAcc) _ = sb.TestStore.AddTestBlock(8642) lockTime := sb.CurrentHeight() - t.Run("Invalid transaction, Should returns error", func(t *testing.T) { - trx := tx.NewTransferTx(lockTime, ts.RandAccAddress(), ts.RandAccAddress(), 1000, 0.1e9, "invalid-tx") - err := exe.Execute(trx, sb) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) - }) + t.Run("Invalid transaction, Should return error", func(t *testing.T) { + randAddr := ts.RandAccAddress() + trx := tx.NewTransferTx(lockTime, randAddr, ts.RandAccAddress(), + ts.RandAmount(), ts.RandFee(), "invalid-tx") - t.Run("Invalid fee (subsidy tx), Should returns error", func(t *testing.T) { - trx := tx.NewTransferTx(lockTime, crypto.TreasuryAddress, ts.RandAccAddress(), 1000, 1, "invalid fee") - - expectedErr := InvalidFeeError{Fee: 1, Expected: 0} - assert.ErrorIs(t, exe.Execute(trx, sb), expectedErr) - assert.ErrorIs(t, exe.checkFee(trx), expectedErr) + err := Execute(trx, sb) + assert.ErrorIs(t, err, executor.AccountNotFoundError{Address: randAddr}) }) - t.Run("Execution failed", func(t *testing.T) { - proof := ts.RandProof() - trx := tx.NewSortitionTx(lockTime, rndValAddr, proof) - ts.HelperSignTransaction(rndPrvKey, trx) - err := exe.Execute(trx, sb) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) + t.Run("Ok", func(t *testing.T) { + trx := tx.NewSubsidyTx(lockTime, ts.RandAccAddress(), 1000, "valid-tx") + err := Execute(trx, sb) + assert.NoError(t, err) + + assert.True(t, sb.AnyRecentTransaction(trx.ID())) }) } -func TestReplay(t *testing.T) { +func TestCheck(t *testing.T) { ts := testsuite.NewTestSuite(t) - executor := NewExecutor() sb := sandbox.MockingSandbox(ts) - rndPubKey, rndPrvKey := ts.RandBLSKeyPair() - rndAccAddr := rndPubKey.AccountAddress() - rndAcc := sb.MakeNewAccount(rndAccAddr) - rndAcc.AddToBalance(1e9) - sb.UpdateAccount(rndAccAddr, rndAcc) + _ = sb.TestStore.AddTestBlock(8642) lockTime := sb.CurrentHeight() - trx := tx.NewTransferTx(lockTime, - rndAccAddr, ts.RandAccAddress(), 10000, 1000, "") - ts.HelperSignTransaction(rndPrvKey, trx) + t.Run("Invalid lock-time, Should return error", func(t *testing.T) { + invalidLocoTme := lockTime + 1 + trx := tx.NewTransferTx(invalidLocoTme, crypto.TreasuryAddress, + ts.RandAccAddress(), ts.RandAmount(), 0, "invalid lock-time") - err := executor.Execute(trx, sb) - assert.NoError(t, err) - err = executor.Execute(trx, sb) - assert.ErrorIs(t, err, TransactionCommittedError{ - ID: trx.ID(), + err := CheckAndExecute(trx, sb, true) + assert.ErrorIs(t, err, LockTimeInFutureError{LockTime: invalidLocoTme}) + }) + + t.Run("Invalid fee, Should return error", func(t *testing.T) { + invalidFee := amount.Amount(1) + trx := tx.NewTransferTx(lockTime, crypto.TreasuryAddress, + ts.RandAccAddress(), ts.RandAmount(), invalidFee, "invalid fee") + + err := CheckAndExecute(trx, sb, true) + assert.ErrorIs(t, err, InvalidFeeError{Fee: invalidFee, Expected: 0}) + }) + + t.Run("Invalid transaction, Should return error", func(t *testing.T) { + randAddr := ts.RandAccAddress() + trx := tx.NewTransferTx(lockTime, randAddr, ts.RandAccAddress(), + ts.RandAmount(), ts.RandFee(), "invalid-tx") + + err := CheckAndExecute(trx, sb, true) + assert.ErrorIs(t, err, executor.AccountNotFoundError{Address: randAddr}) + }) + + t.Run("Invalid transaction, Should return error", func(t *testing.T) { + valAddr := sb.TestCommittee.Validators()[0].Address() + sb.TestAcceptSortition = false + trx := tx.NewSortitionTx(lockTime, valAddr, ts.RandProof()) + + err := CheckAndExecute(trx, sb, true) + assert.ErrorIs(t, err, executor.ErrInvalidSortitionProof) + }) + + t.Run("Ok", func(t *testing.T) { + trx := tx.NewSubsidyTx(lockTime, ts.RandAccAddress(), 1000, "valid-tx") + err := CheckAndExecute(trx, sb, true) + assert.NoError(t, err) + + assert.True(t, sb.AnyRecentTransaction(trx.ID())) }) } -func TestChecker(t *testing.T) { +func TestReplay(t *testing.T) { ts := testsuite.NewTestSuite(t) - executor := NewExecutor() - checker := NewChecker() sb := sandbox.MockingSandbox(ts) rndPubKey, rndPrvKey := ts.RandBLSKeyPair() rndAccAddr := rndPubKey.AccountAddress() rndAcc := sb.MakeNewAccount(rndAccAddr) rndAcc.AddToBalance(1e9) sb.UpdateAccount(rndAccAddr, rndAcc) - lockTime := sb.CurrentHeight() + 1 + lockTime := sb.CurrentHeight() trx := tx.NewTransferTx(lockTime, rndAccAddr, ts.RandAccAddress(), 10000, 1000, "") ts.HelperSignTransaction(rndPrvKey, trx) - err := executor.Execute(trx, sb) - assert.ErrorIs(t, err, FutureLockTimeError{LockTime: lockTime}) - err = checker.Execute(trx, sb) + err := Execute(trx, sb) assert.NoError(t, err) + + err = CheckAndExecute(trx, sb, false) + assert.ErrorIs(t, err, TransactionCommittedError{ + ID: trx.ID(), + }) } diff --git a/execution/executor/bond.go b/execution/executor/bond.go index a019c0b00..01449aa85 100644 --- a/execution/executor/bond.go +++ b/execution/executor/bond.go @@ -2,94 +2,101 @@ package executor import ( "github.com/pactus-project/pactus/sandbox" + "github.com/pactus-project/pactus/types/account" + "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" - "github.com/pactus-project/pactus/util/errors" + "github.com/pactus-project/pactus/types/validator" ) type BondExecutor struct { - strict bool + sb sandbox.Sandbox + pld *payload.BondPayload + fee amount.Amount + sender *account.Account + receiver *validator.Validator } -func NewBondExecutor(strict bool) *BondExecutor { - return &BondExecutor{strict: strict} -} - -func (e *BondExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func newBondExecutor(trx *tx.Tx, sb sandbox.Sandbox) (*BondExecutor, error) { pld := trx.Payload().(*payload.BondPayload) - senderAcc := sb.Account(pld.From) - if senderAcc == nil { - return errors.Errorf(errors.ErrInvalidAddress, - "unable to retrieve sender account") + sender := sb.Account(pld.From) + if sender == nil { + return nil, AccountNotFoundError{Address: pld.From} } - receiverVal := sb.Validator(pld.To) - if receiverVal == nil { + receiver := sb.Validator(pld.To) + if receiver == nil { if pld.PublicKey == nil { - return errors.Errorf(errors.ErrInvalidPublicKey, - "public key is not set") + return nil, ErrPublicKeyNotSet } - // TODO: remove me in future - if pld.Stake < sb.Params().MinimumStake { - return errors.Errorf(errors.ErrInvalidTx, - "validator's stake can't be less than %v", sb.Params().MinimumStake) - } - receiverVal = sb.MakeNewValidator(pld.PublicKey) + receiver = sb.MakeNewValidator(pld.PublicKey) } else if pld.PublicKey != nil { - return errors.Errorf(errors.ErrInvalidPublicKey, - "public key is set") + return nil, ErrPublicKeyAlreadySet + } + + return &BondExecutor{ + sb: sb, + pld: pld, + fee: trx.Fee(), + sender: sender, + receiver: receiver, + }, nil +} + +func (e *BondExecutor) Check(strict bool) error { + if e.receiver.UnbondingHeight() > 0 { + return ErrValidatorUnbonded } - if receiverVal.UnbondingHeight() > 0 { - return errors.Errorf(errors.ErrInvalidHeight, - "validator has unbonded at height %v", receiverVal.UnbondingHeight()) + + if e.sender.Balance() < e.pld.Stake+e.fee { + return ErrInsufficientFunds } - if e.strict { + + if e.pld.Stake < e.sb.Params().MinimumStake { + // This check prevents a potential attack where an attacker could send zero + // or a small amount of stake to a full validator, effectively parking the + // validator for the bonding period. + if e.pld.Stake == 0 || e.pld.Stake+e.receiver.Stake() != e.sb.Params().MaximumStake { + return SmallStakeError{ + Minimum: e.sb.Params().MinimumStake, + } + } + } + + if e.receiver.Stake()+e.pld.Stake > e.sb.Params().MaximumStake { + return MaximumStakeError{ + Maximum: e.sb.Params().MaximumStake, + } + } + + if strict { // In strict mode, bond transactions will be rejected if a validator is // already in the committee. // In non-strict mode, they are added to the transaction pool and // processed once eligible. - if sb.Committee().Contains(pld.To) { - return errors.Errorf(errors.ErrInvalidTx, - "validator %v is in committee", pld.To) + if e.sb.Committee().Contains(e.pld.To) { + return ErrValidatorInCommittee } // In strict mode, bond transactions will be rejected if a validator is // going to join the committee in the next height. // In non-strict mode, they are added to the transaction pool and // processed once eligible. - if sb.IsJoinedCommittee(pld.To) { - return errors.Errorf(errors.ErrInvalidTx, - "validator %v joins committee in the next height", pld.To) + if e.sb.IsJoinedCommittee(e.pld.To) { + return ErrValidatorInCommittee } } - if senderAcc.Balance() < pld.Stake+trx.Fee() { - return ErrInsufficientFunds - } - if receiverVal.Stake()+pld.Stake > sb.Params().MaximumStake { - return errors.Errorf(errors.ErrInvalidAmount, - "validator's stake can't be more than %v", sb.Params().MaximumStake) - } - // TODO: remove me in future - // We can have a level for committing blocks even if they are not fully compatible with the current rules. - // However, since they were committed in the past, they should be accepted by new nodes. - if sb.CurrentHeight() > 740_000 { - if pld.Stake < sb.Params().MinimumStake { - if pld.Stake == 0 || receiverVal.Stake()+pld.Stake != sb.Params().MaximumStake { - return errors.Errorf(errors.ErrInvalidTx, - "stake amount should not be less than %v", sb.Params().MinimumStake) - } - } - } - - senderAcc.SubtractFromBalance(pld.Stake + trx.Fee()) - receiverVal.AddToStake(pld.Stake) - receiverVal.UpdateLastBondingHeight(sb.CurrentHeight()) + return nil +} - sb.UpdatePowerDelta(int64(pld.Stake)) - sb.UpdateAccount(pld.From, senderAcc) - sb.UpdateValidator(receiverVal) +func (e *BondExecutor) Execute() { + e.sender.SubtractFromBalance(e.pld.Stake + e.fee) + e.receiver.AddToStake(e.pld.Stake) + e.receiver.UpdateLastBondingHeight(e.sb.CurrentHeight()) - return nil + e.sb.UpdatePowerDelta(int64(e.pld.Stake)) + e.sb.UpdateAccount(e.pld.From, e.sender) + e.sb.UpdateValidator(e.receiver) } diff --git a/execution/executor/bond_test.go b/execution/executor/bond_test.go index b5b4b73f8..80b9532a2 100644 --- a/execution/executor/bond_test.go +++ b/execution/executor/bond_test.go @@ -3,186 +3,129 @@ package executor import ( "testing" - "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/util/errors" "github.com/stretchr/testify/assert" ) func TestExecuteBondTx(t *testing.T) { td := setup(t) - exe := NewBondExecutor(true) senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc() senderBalance := senderAcc.Balance() - pub, _ := td.RandBLSKeyPair() - receiverAddr := pub.ValidatorAddress() + valPub, _ := td.RandBLSKeyPair() + receiverAddr := valPub.ValidatorAddress() + amt := td.RandAmountRange( td.sandbox.TestParams.MinimumStake, td.sandbox.TestParams.MaximumStake) fee := td.RandFee() lockTime := td.sandbox.CurrentHeight() - t.Run("Should fail, invalid sender", func(t *testing.T) { - trx := tx.NewBondTx(lockTime, td.RandAccAddress(), - receiverAddr, pub, amt, fee, "invalid sender") + t.Run("Should fail, unknown address", func(t *testing.T) { + randomAddr := td.RandAccAddress() + trx := tx.NewBondTx(lockTime, randomAddr, + receiverAddr, valPub, amt, fee, "unknown address") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) + td.check(t, trx, true, AccountNotFoundError{Address: randomAddr}) + td.check(t, trx, false, AccountNotFoundError{Address: randomAddr}) }) - t.Run("Should fail, treasury address as receiver", func(t *testing.T) { + t.Run("Should fail, public key is not set", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, - crypto.TreasuryAddress, nil, amt, fee, "invalid ") + receiverAddr, nil, amt, fee, "no public key") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidPublicKey) + td.check(t, trx, true, ErrPublicKeyNotSet) + td.check(t, trx, false, ErrPublicKeyNotSet) }) - t.Run("Should fail, insufficient balance", func(t *testing.T) { + t.Run("Should fail, public key should not set for existing validators", func(t *testing.T) { + randPub, _ := td.RandBLSKeyPair() + val := td.sandbox.MakeNewValidator(randPub) + td.sandbox.UpdateValidator(val) + trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, pub, senderBalance+1, 0, "insufficient balance") + randPub.ValidatorAddress(), randPub, amt, fee, "with public key") - err := exe.Execute(trx, td.sandbox) - assert.ErrorIs(t, err, ErrInsufficientFunds) + td.check(t, trx, true, ErrPublicKeyAlreadySet) + td.check(t, trx, false, ErrPublicKeyAlreadySet) }) - t.Run("Should fail, inside committee", func(t *testing.T) { - pub0 := td.sandbox.Committee().Proposer(0).PublicKey() + t.Run("Should fail, insufficient balance", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, - pub0.ValidatorAddress(), nil, amt, fee, "inside committee") + receiverAddr, valPub, senderBalance+1, 0, "insufficient balance") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) + td.check(t, trx, true, ErrInsufficientFunds) + td.check(t, trx, false, ErrInsufficientFunds) }) t.Run("Should fail, unbonded before", func(t *testing.T) { - unbondedPub, _ := td.RandBLSKeyPair() - val := td.sandbox.MakeNewValidator(unbondedPub) - val.UpdateLastBondingHeight(1) - val.UpdateUnbondingHeight(td.sandbox.CurrentHeight()) + randPub, _ := td.RandBLSKeyPair() + val := td.sandbox.MakeNewValidator(randPub) + val.UpdateUnbondingHeight(td.RandHeight()) td.sandbox.UpdateValidator(val) trx := tx.NewBondTx(lockTime, senderAddr, - unbondedPub.ValidatorAddress(), nil, amt, fee, "unbonded before") + randPub.ValidatorAddress(), nil, amt, fee, "unbonded before") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidHeight) + td.check(t, trx, true, ErrValidatorUnbonded) + td.check(t, trx, false, ErrValidatorUnbonded) }) - t.Run("Should fail, public key is not set", func(t *testing.T) { + t.Run("Should fail, amount less than MinimumStake", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, nil, amt, fee, "no public key") + receiverAddr, valPub, td.sandbox.TestParams.MinimumStake-1, fee, "less than MinimumStake") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidPublicKey) + td.check(t, trx, true, SmallStakeError{td.sandbox.TestParams.MinimumStake}) + td.check(t, trx, false, SmallStakeError{td.sandbox.TestParams.MinimumStake}) }) - t.Run("Should fail, amount less than MinimumStake", func(t *testing.T) { + t.Run("Should fail, validator's stake exceeds the MaximumStake", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, pub, 1000, fee, "less than MinimumStake") + receiverAddr, valPub, td.sandbox.TestParams.MaximumStake+1, fee, "more than MaximumStake") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.ErrInvalidTx, errors.Code(err)) + td.check(t, trx, true, MaximumStakeError{td.sandbox.TestParams.MaximumStake}) + td.check(t, trx, false, MaximumStakeError{td.sandbox.TestParams.MaximumStake}) }) - t.Run("Ok", func(t *testing.T) { + t.Run("Should fail, inside committee", func(t *testing.T) { + pub0 := td.sandbox.Committee().Proposer(0).PublicKey() trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, pub, amt, fee, "ok") + pub0.ValidatorAddress(), nil, amt, fee, "inside committee") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err, "Ok") + td.check(t, trx, true, ErrValidatorInCommittee) + td.check(t, trx, false, nil) }) - t.Run("Should fail, public key should not set for existing validators", func(t *testing.T) { + t.Run("Should fail, joining committee", func(t *testing.T) { + randPub, _ := td.RandBLSKeyPair() + val := td.sandbox.MakeNewValidator(randPub) + td.sandbox.UpdateValidator(val) + td.sandbox.JoinedToCommittee(val.Address()) trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, pub, amt, fee, "with public key") + randPub.ValidatorAddress(), nil, amt, fee, "inside committee") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidPublicKey) + td.check(t, trx, true, ErrValidatorInCommittee) + td.check(t, trx, false, nil) }) - assert.Equal(t, td.sandbox.Account(senderAddr).Balance(), senderBalance-(amt+fee)) - assert.Equal(t, td.sandbox.Validator(receiverAddr).Stake(), amt) - assert.Equal(t, td.sandbox.Validator(receiverAddr).LastBondingHeight(), td.sandbox.CurrentHeight()) - assert.Equal(t, td.sandbox.PowerDelta(), int64(amt)) - td.checkTotalCoin(t, fee) -} - -// TestBondInsideCommittee checks if a validator inside the committee attempts to -// increase their stake. -// In non-strict mode it should be accepted. -func TestBondInsideCommittee(t *testing.T) { - td := setup(t) - - exe1 := NewBondExecutor(true) - exe2 := NewBondExecutor(false) - senderAddr, _ := td.sandbox.TestStore.RandomTestAcc() - amt := td.RandAmountRange( - td.sandbox.TestParams.MinimumStake, - td.sandbox.TestParams.MaximumStake-10e9) // it has 10e9 stake - fee := td.RandFee() - lockTime := td.sandbox.CurrentHeight() - - pub := td.sandbox.Committee().Proposer(0).PublicKey() - trx := tx.NewBondTx(lockTime, senderAddr, - pub.ValidatorAddress(), nil, amt, fee, "inside committee") - - assert.Error(t, exe1.Execute(trx, td.sandbox)) - assert.NoError(t, exe2.Execute(trx, td.sandbox)) -} - -// TestBondJoiningCommittee checks if a validator attempts to increase their -// stake after evaluating sortition. -// In non-strict mode, it should be accepted. -func TestBondJoiningCommittee(t *testing.T) { - td := setup(t) - - exe1 := NewBondExecutor(true) - exe2 := NewBondExecutor(false) - senderAddr, _ := td.sandbox.TestStore.RandomTestAcc() - pub, _ := td.RandBLSKeyPair() - amt := td.RandAmountRange( - td.sandbox.TestParams.MinimumStake, - td.sandbox.TestParams.MaximumStake-10e9) // it has 10e9 stake - fee := td.RandFee() - lockTime := td.sandbox.CurrentHeight() - - val := td.sandbox.MakeNewValidator(pub) - val.UpdateLastBondingHeight(1) - val.UpdateLastSortitionHeight(td.sandbox.CurrentHeight()) - td.sandbox.UpdateValidator(val) - td.sandbox.JoinedToCommittee(val.Address()) - - trx := tx.NewBondTx(lockTime, senderAddr, - pub.ValidatorAddress(), nil, amt, fee, "joining committee") - - assert.Error(t, exe1.Execute(trx, td.sandbox)) - assert.NoError(t, exe2.Execute(trx, td.sandbox)) -} - -// TestStakeExceeded checks if the validator's stake exceeded the MaximumStake parameter. -func TestStakeExceeded(t *testing.T) { - td := setup(t) + t.Run("Ok", func(t *testing.T) { + trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, valPub, amt, fee, "ok") - exe := NewBondExecutor(true) - amt := td.sandbox.TestParams.MaximumStake + 1 - fee := td.RandFee() - senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc() - senderAcc.AddToBalance(td.sandbox.TestParams.MaximumStake + 1) - td.sandbox.UpdateAccount(senderAddr, senderAcc) - pub, _ := td.RandBLSKeyPair() - lockTime := td.sandbox.CurrentHeight() + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) + }) - trx := tx.NewBondTx(lockTime, senderAddr, - pub.ValidatorAddress(), pub, amt, fee, "stake exceeded") + updatedSenderAcc := td.sandbox.Account(senderAddr) + updatedReceiverVal := td.sandbox.Validator(receiverAddr) + assert.Equal(t, senderBalance-(amt+fee), updatedSenderAcc.Balance()) + assert.Equal(t, amt, updatedReceiverVal.Stake()) + assert.Equal(t, lockTime, updatedReceiverVal.LastBondingHeight()) - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAmount) + td.checkTotalCoin(t, fee) } func TestPowerDeltaBond(t *testing.T) { td := setup(t) - exe := NewBondExecutor(true) senderAddr, _ := td.sandbox.TestStore.RandomTestAcc() pub, _ := td.RandBLSKeyPair() @@ -192,11 +135,9 @@ func TestPowerDeltaBond(t *testing.T) { td.sandbox.TestParams.MaximumStake) fee := td.RandFee() lockTime := td.sandbox.CurrentHeight() - trx := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, pub, amt, fee, "ok") + trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, pub, amt, fee, "ok") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err, "Ok") + td.execute(t, trx) assert.Equal(t, int64(amt), td.sandbox.PowerDelta()) } @@ -206,52 +147,49 @@ func TestPowerDeltaBond(t *testing.T) { // https://github.com/pactus-project/pactus/issues/1223 func TestSmallBond(t *testing.T) { td := setup(t) - exe := NewBondExecutor(false) - td.sandbox.TestStore.AddTestBlock(752000 + 1) // TODO: remove me in future senderAddr, _ := td.sandbox.TestStore.RandomTestAcc() - receiverVal := td.sandbox.TestStore.RandomTestVal() - receiverAddr := receiverVal.Address() - fee := td.RandFee() + receiverPub, _ := td.RandBLSKeyPair() + receiverAddr := receiverPub.ValidatorAddress() + receiverVal := td.sandbox.MakeNewValidator(receiverPub) + receiverVal.AddToStake(td.sandbox.TestParams.MaximumStake - 2) + td.sandbox.UpdateValidator(receiverVal) lockTime := td.sandbox.CurrentHeight() - trxBond := tx.NewBondTx(lockTime, senderAddr, - receiverAddr, nil, 1000e9-receiverVal.Stake()-2, fee, "ok") - - err := exe.Execute(trxBond, td.sandbox) - assert.NoError(t, err, "Ok") + fee := td.RandFee() t.Run("Rejects bond transaction with zero amount", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, nil, 0, fee, "attacking validator") - err := exe.Execute(trx, td.sandbox) - assert.Error(t, err, "Zero bond amount should be rejected") + td.check(t, trx, true, SmallStakeError{td.sandbox.TestParams.MinimumStake}) + td.check(t, trx, false, SmallStakeError{td.sandbox.TestParams.MinimumStake}) }) t.Run("Rejects bond transaction below full validator stake", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, nil, 1, fee, "attacking validator") - err := exe.Execute(trx, td.sandbox) - assert.Error(t, err, "Bond amount below full stake should be rejected") + td.check(t, trx, true, SmallStakeError{td.sandbox.TestParams.MinimumStake}) + td.check(t, trx, false, SmallStakeError{td.sandbox.TestParams.MinimumStake}) }) t.Run("Accepts bond transaction reaching full validator stake", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, nil, 2, fee, "fulfilling validator stake") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err, "Bond reaching full stake should be accepted") + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) }) - t.Run("Accepts bond transaction with zero amount on full validator", func(t *testing.T) { + t.Run("Rejects bond transaction with zero amount on full validator", func(t *testing.T) { trx := tx.NewBondTx(lockTime, senderAddr, receiverAddr, nil, 0, fee, "attacking validator") - err := exe.Execute(trx, td.sandbox) - assert.Error(t, err, "Zero bond amount on full stake should be rejected") + td.check(t, trx, true, SmallStakeError{td.sandbox.TestParams.MinimumStake}) + td.check(t, trx, false, SmallStakeError{td.sandbox.TestParams.MinimumStake}) }) - val, _ := td.sandbox.TestStore.Validator(receiverVal.Address()) - assert.Equal(t, td.sandbox.Params().MaximumStake, val.Stake()) + receiverValAfterExecution, _ := td.sandbox.TestStore.Validator(receiverVal.Address()) + assert.Equal(t, td.sandbox.Params().MaximumStake, receiverValAfterExecution.Stake()) } diff --git a/execution/executor/errors.go b/execution/executor/errors.go index 3ed28c988..d67d3f15b 100644 --- a/execution/executor/errors.go +++ b/execution/executor/errors.go @@ -1,6 +1,98 @@ package executor -import "errors" +import ( + "errors" + "fmt" -// ErrInsufficientFunds indicates the balance is low for the transaction. + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/types/amount" + "github.com/pactus-project/pactus/types/tx/payload" +) + +// ErrInsufficientFunds indicates that the balance is insufficient for the transaction. var ErrInsufficientFunds = errors.New("insufficient funds") + +// ErrPublicKeyNotSet indicates that the public key is not set for the initial Bond transaction. +var ErrPublicKeyNotSet = errors.New("public key is not set") + +// ErrPublicKeyAlreadySet indicates that the public key has already been set for the given validator. +var ErrPublicKeyAlreadySet = errors.New("public key is already set") + +// ErrValidatorBonded indicates that the validator is bonded. +var ErrValidatorBonded = errors.New("validator is bonded") + +// ErrValidatorUnbonded indicates that the validator has unbonded. +var ErrValidatorUnbonded = errors.New("validator has unbonded") + +// ErrBondingPeriod is returned when a validator is in the bonding period. +var ErrBondingPeriod = errors.New("validator in bonding period") + +// ErrUnbondingPeriod is returned when a validator is in the unbonding period. +var ErrUnbondingPeriod = errors.New("validator in unbonding period") + +// ErrInvalidSortitionProof indicates that the sortition proof is invalid. +var ErrInvalidSortitionProof = errors.New("invalid sortition proof") + +// ErrExpiredSortition indicates that the sortition transaction is duplicated or expired. +var ErrExpiredSortition = errors.New("expired sortition") + +// ErrValidatorInCommittee indicates that the validator is in the committee. +var ErrValidatorInCommittee = errors.New("validator is in the committee") + +// ErrCommitteeJoinLimitExceeded indicates that at each height, +// the maximum stake joining the committee can't be more than 1/3 of the committee's total stake. +var ErrCommitteeJoinLimitExceeded = errors.New( + "the maximum stake joining the committee can't be more than 1/3 of the committee's total stake") + +// ErrCommitteeLeaveLimitExceeded indicates that at each height, +// the maximum stake leaving the committee can't be more than 1/3 of the committee's total stake. +var ErrCommitteeLeaveLimitExceeded = errors.New( + "the maximum stake leaving the committee can't be more than 1/3 of the committee's total stake") + +// ErrOldestValidatorNotProposed indicates that the oldest validator has not proposed any block yet. +var ErrOldestValidatorNotProposed = errors.New("oldest validator has not proposed any block yet") + +// SmallStakeError is returned when the stake amount is less than the minimum stake. +type SmallStakeError struct { + Minimum amount.Amount +} + +func (e SmallStakeError) Error() string { + return fmt.Sprintf("stake amount can't be less than %v", e.Minimum.String()) +} + +// MaximumStakeError is returned when the validator's stake exceeds the maximum stake limit. +type MaximumStakeError struct { + Maximum amount.Amount +} + +func (e MaximumStakeError) Error() string { + return fmt.Sprintf("validator's stake amount can't be more than %v", e.Maximum.String()) +} + +// InvalidPayloadTypeError is returned when the transaction payload type is not valid. +type InvalidPayloadTypeError struct { + PayloadType payload.Type +} + +func (e InvalidPayloadTypeError) Error() string { + return fmt.Sprintf("unknown payload type: %s", e.PayloadType.String()) +} + +// AccountNotFoundError is raised when the given address has no associated account. +type AccountNotFoundError struct { + Address crypto.Address +} + +func (e AccountNotFoundError) Error() string { + return fmt.Sprintf("no account found for address: %s", e.Address.String()) +} + +// ValidatorNotFoundError is raised when the given address has no associated validator. +type ValidatorNotFoundError struct { + Address crypto.Address +} + +func (e ValidatorNotFoundError) Error() string { + return fmt.Sprintf("no validator found for address: %s", e.Address.String()) +} diff --git a/execution/executor/executor.go b/execution/executor/executor.go new file mode 100644 index 000000000..a608557bb --- /dev/null +++ b/execution/executor/executor.go @@ -0,0 +1,35 @@ +package executor + +import ( + "github.com/pactus-project/pactus/sandbox" + "github.com/pactus-project/pactus/types/tx" + "github.com/pactus-project/pactus/types/tx/payload" +) + +type Executor interface { + Check(strict bool) error + Execute() +} + +func MakeExecutor(trx *tx.Tx, sb sandbox.Sandbox) (Executor, error) { + var exe Executor + var err error + switch t := trx.Payload().Type(); t { + case payload.TypeTransfer: + exe, err = newTransferExecutor(trx, sb) + case payload.TypeBond: + exe, err = newBondExecutor(trx, sb) + case payload.TypeUnbond: + exe, err = newUnbondExecutor(trx, sb) + case payload.TypeWithdraw: + exe, err = newWithdrawExecutor(trx, sb) + case payload.TypeSortition: + exe, err = newSortitionExecutor(trx, sb) + default: + return nil, InvalidPayloadTypeError{ + PayloadType: t, + } + } + + return exe, err +} diff --git a/execution/executor/executor_test.go b/execution/executor/executor_test.go new file mode 100644 index 000000000..82386d9ca --- /dev/null +++ b/execution/executor/executor_test.go @@ -0,0 +1,70 @@ +package executor + +import ( + "testing" + + "github.com/pactus-project/pactus/sandbox" + "github.com/pactus-project/pactus/types/amount" + "github.com/pactus-project/pactus/types/tx" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testData struct { + *testsuite.TestSuite + + sandbox *sandbox.MockSandbox +} + +func setup(t *testing.T) *testData { + t.Helper() + + ts := testsuite.NewTestSuite(t) + + sb := sandbox.MockingSandbox(ts) + randHeight := ts.RandHeight() + _ = sb.TestStore.AddTestBlock(randHeight) + + return &testData{ + TestSuite: ts, + sandbox: sb, + } +} + +func (td *testData) checkTotalCoin(t *testing.T, fee amount.Amount) { + t.Helper() + + total := amount.Amount(0) + for _, acc := range td.sandbox.TestStore.Accounts { + total += acc.Balance() + } + + for _, val := range td.sandbox.TestStore.Validators { + total += val.Stake() + } + assert.Equal(t, total+fee, amount.Amount(21_000_000*1e9)) +} + +func (td *testData) check(t *testing.T, trx *tx.Tx, strict bool, expectedErr error) { + t.Helper() + + exe, err := MakeExecutor(trx, td.sandbox) + if err != nil { + assert.ErrorIs(t, err, expectedErr) + + return + } + + err = exe.Check(strict) + assert.ErrorIs(t, err, expectedErr) +} + +func (td *testData) execute(t *testing.T, trx *tx.Tx) { + t.Helper() + + exe, err := MakeExecutor(trx, td.sandbox) + require.NoError(t, err) + + exe.Execute() +} diff --git a/execution/executor/sortition.go b/execution/executor/sortition.go index fc0e6a0f4..dedf6a7b1 100644 --- a/execution/executor/sortition.go +++ b/execution/executor/sortition.go @@ -7,75 +7,72 @@ import ( "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" "github.com/pactus-project/pactus/types/validator" - "github.com/pactus-project/pactus/util/errors" ) type SortitionExecutor struct { - strict bool + sb sandbox.Sandbox + pld *payload.SortitionPayload + validator *validator.Validator + sortitionHeight uint32 } -func NewSortitionExecutor(strict bool) *SortitionExecutor { - return &SortitionExecutor{strict: strict} -} - -func (e *SortitionExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func newSortitionExecutor(trx *tx.Tx, sb sandbox.Sandbox) (*SortitionExecutor, error) { pld := trx.Payload().(*payload.SortitionPayload) val := sb.Validator(pld.Validator) if val == nil { - return errors.Errorf(errors.ErrInvalidAddress, - "unable to retrieve validator") + return nil, ValidatorNotFoundError{ + Address: pld.Validator, + } } - if sb.CurrentHeight()-val.LastBondingHeight() < sb.Params().BondInterval { - return errors.Errorf(errors.ErrInvalidHeight, - "validator has bonded at height %v", val.LastBondingHeight()) + return &SortitionExecutor{ + pld: pld, + sb: sb, + validator: val, + sortitionHeight: trx.LockTime(), + }, nil +} + +func (e *SortitionExecutor) Check(strict bool) error { + if e.sb.CurrentHeight()-e.validator.LastBondingHeight() < e.sb.Params().BondInterval { + return ErrBondingPeriod } - sortitionHeight := trx.LockTime() - ok := sb.VerifyProof(sortitionHeight, pld.Proof, val) + ok := e.sb.VerifyProof(e.sortitionHeight, e.pld.Proof, e.validator) if !ok { - return errors.Error(errors.ErrInvalidProof) + return ErrInvalidSortitionProof } // Check for the duplicated or expired sortition transactions - if sortitionHeight <= val.LastSortitionHeight() { - return errors.Errorf(errors.ErrInvalidTx, - "duplicated sortition transaction") + if e.sortitionHeight <= e.validator.LastSortitionHeight() { + return ErrExpiredSortition } - if e.strict { - if err := e.joinCommittee(sb, val); err != nil { + if strict { + if err := e.canJoinCommittee(); err != nil { return err } } - val.UpdateLastSortitionHeight(sortitionHeight) - - sb.JoinedToCommittee(pld.Validator) - sb.UpdateValidator(val) - return nil } -func (*SortitionExecutor) joinCommittee(sb sandbox.Sandbox, - val *validator.Validator, -) error { - if sb.Committee().Size() < sb.Params().CommitteeSize { +func (e *SortitionExecutor) canJoinCommittee() error { + if e.sb.Committee().Size() < e.sb.Params().CommitteeSize { // There are available seats in the committee. - if sb.Committee().Contains(val.Address()) { - return errors.Errorf(errors.ErrInvalidTx, - "validator is in committee") + if e.sb.Committee().Contains(e.pld.Validator) { + return ErrValidatorInCommittee } return nil } - // The committee is full, check if the validator can enter the committee. + // The committee is full, check if the validator can join the committee. joiningNum := 0 joiningPower := int64(0) - committee := sb.Committee() - sb.IterateValidators(func(val *validator.Validator, _ bool, joined bool) { + committee := e.sb.Committee() + e.sb.IterateValidators(func(val *validator.Validator, _ bool, joined bool) { if joined { if !committee.Contains(val.Address()) { joiningPower += val.Power() @@ -83,13 +80,12 @@ func (*SortitionExecutor) joinCommittee(sb sandbox.Sandbox, } } }) - if !committee.Contains(val.Address()) { - joiningPower += val.Power() + if !committee.Contains(e.pld.Validator) { + joiningPower += e.validator.Power() joiningNum++ } if joiningPower >= (committee.TotalPower() / 3) { - return errors.Errorf(errors.ErrInvalidTx, - "in each height only 1/3 of stake can join") + return ErrCommitteeJoinLimitExceeded } vals := committee.Validators() @@ -101,11 +97,10 @@ func (*SortitionExecutor) joinCommittee(sb sandbox.Sandbox, leavingPower += vals[i].Power() } if leavingPower >= (committee.TotalPower() / 3) { - return errors.Errorf(errors.ErrInvalidTx, - "in each height only 1/3 of stake can leave") + return ErrCommitteeLeaveLimitExceeded } - oldestSortitionHeight := sb.CurrentHeight() + oldestSortitionHeight := e.sb.CurrentHeight() for _, v := range committee.Validators() { if v.LastSortitionHeight() < oldestSortitionHeight { oldestSortitionHeight = v.LastSortitionHeight() @@ -113,12 +108,18 @@ func (*SortitionExecutor) joinCommittee(sb sandbox.Sandbox, } // If the oldest validator in the committee still hasn't propose a block yet, - // she stays in the committee. - proposerHeight := sb.Committee().Proposer(0).LastSortitionHeight() + // it stays in the committee. + proposerHeight := e.sb.Committee().Proposer(0).LastSortitionHeight() if oldestSortitionHeight >= proposerHeight { - return errors.Errorf(errors.ErrInvalidTx, - "oldest validator still didn't propose any block") + return ErrOldestValidatorNotProposed } return nil } + +func (e *SortitionExecutor) Execute() { + e.validator.UpdateLastSortitionHeight(e.sortitionHeight) + + e.sb.JoinedToCommittee(e.pld.Validator) + e.sb.UpdateValidator(e.validator) +} diff --git a/execution/executor/sortition_test.go b/execution/executor/sortition_test.go index c96d8f501..bd2c75fd5 100644 --- a/execution/executor/sortition_test.go +++ b/execution/executor/sortition_test.go @@ -7,7 +7,6 @@ import ( "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/validator" - "github.com/pactus-project/pactus/util/errors" "github.com/stretchr/testify/assert" ) @@ -25,103 +24,104 @@ func updateCommittee(td *testData) { func TestExecuteSortitionTx(t *testing.T) { td := setup(t) - exe := NewSortitionExecutor(true) - - existingVal := td.sandbox.TestStore.RandomTestVal() - pub, _ := td.RandBLSKeyPair() - newVal := td.sandbox.MakeNewValidator(pub) - accAddr, acc := td.sandbox.TestStore.RandomTestAcc() - amt := td.RandAmountRange(0, acc.Balance()) - fee := td.RandFee() - newVal.AddToStake(amt + fee) - acc.SubtractFromBalance(amt + fee) - td.sandbox.UpdateAccount(accAddr, acc) - td.sandbox.UpdateValidator(newVal) + + bonderAddr, bonderAcc := td.sandbox.TestStore.RandomTestAcc() + bonderBalance := bonderAcc.Balance() + stake := td.RandAmountRange( + td.sandbox.TestParams.MinimumStake, + bonderBalance) + bonderAcc.SubtractFromBalance(stake) + td.sandbox.UpdateAccount(bonderAddr, bonderAcc) + + valPub, _ := td.RandBLSKeyPair() + valAddr := valPub.ValidatorAddress() + val := td.sandbox.MakeNewValidator(valPub) + val.AddToStake(stake) + td.sandbox.UpdateValidator(val) + curHeight := td.sandbox.CurrentHeight() - lockTime := curHeight + lockTime := td.sandbox.CurrentHeight() proof := td.RandProof() - newVal.UpdateLastBondingHeight(curHeight - td.sandbox.Params().BondInterval) - td.sandbox.UpdateValidator(newVal) - assert.Zero(t, td.sandbox.Validator(newVal.Address()).LastSortitionHeight()) - assert.False(t, td.sandbox.IsJoinedCommittee(newVal.Address())) + val.UpdateLastBondingHeight(curHeight - td.sandbox.Params().BondInterval) + td.sandbox.UpdateValidator(val) - t.Run("Should fail, Invalid address", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, td.RandAccAddress(), proof) - td.sandbox.TestAcceptSortition = true - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) + assert.Zero(t, val.LastSortitionHeight()) + assert.False(t, td.sandbox.IsJoinedCommittee(val.Address())) + + t.Run("Should fail, unknown address", func(t *testing.T) { + randomAddr := td.RandAccAddress() + trx := tx.NewSortitionTx(lockTime, randomAddr, proof) + + td.check(t, trx, true, ValidatorNotFoundError{Address: randomAddr}) + td.check(t, trx, false, ValidatorNotFoundError{Address: randomAddr}) }) - newVal.UpdateLastBondingHeight(curHeight - td.sandbox.Params().BondInterval + 1) - td.sandbox.UpdateValidator(newVal) + val.UpdateLastBondingHeight(curHeight - td.sandbox.Params().BondInterval + 1) + td.sandbox.UpdateValidator(val) t.Run("Should fail, Bonding period", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, newVal.Address(), proof) - td.sandbox.TestAcceptSortition = true - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidHeight) + trx := tx.NewSortitionTx(lockTime, val.Address(), proof) + + td.check(t, trx, true, ErrBondingPeriod) + td.check(t, trx, false, ErrBondingPeriod) }) // Let's add one more block td.sandbox.TestStore.AddTestBlock(curHeight + 1) - t.Run("Should fail, Invalid proof", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, newVal.Address(), proof) + t.Run("Should fail, invalid proof", func(t *testing.T) { + trx := tx.NewSortitionTx(lockTime, val.Address(), proof) td.sandbox.TestAcceptSortition = false - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidProof) + + td.check(t, trx, true, ErrInvalidSortitionProof) + td.check(t, trx, false, ErrInvalidSortitionProof) }) - t.Run("Should fail, Committee has free seats and validator is in the committee", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, existingVal.Address(), proof) + t.Run("Should fail, committee has free seats and validator is in the committee", func(t *testing.T) { + val0 := td.sandbox.Committee().Proposer(0) + trx := tx.NewSortitionTx(lockTime, val0.Address(), proof) td.sandbox.TestAcceptSortition = true - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) + + td.check(t, trx, true, ErrValidatorInCommittee) + td.check(t, trx, false, nil) }) t.Run("Should be ok", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, newVal.Address(), proof) + trx := tx.NewSortitionTx(lockTime, val.Address(), proof) td.sandbox.TestAcceptSortition = true - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) + + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) }) - t.Run("Should fail, duplicated sortition", func(t *testing.T) { - trx := tx.NewSortitionTx(lockTime, newVal.Address(), proof) + t.Run("Should fail, expired sortition", func(t *testing.T) { + trx := tx.NewSortitionTx(lockTime-1, val.Address(), proof) td.sandbox.TestAcceptSortition = true - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) + + td.check(t, trx, true, ErrExpiredSortition) + td.check(t, trx, false, ErrExpiredSortition) }) - assert.Equal(t, td.sandbox.Validator(newVal.Address()).LastSortitionHeight(), lockTime) - assert.True(t, td.sandbox.IsJoinedCommittee(newVal.Address())) + t.Run("Should fail, duplicated sortition", func(t *testing.T) { + trx := tx.NewSortitionTx(lockTime, val.Address(), proof) - td.checkTotalCoin(t, 0) -} + td.check(t, trx, true, ErrExpiredSortition) + td.check(t, trx, false, ErrExpiredSortition) + }) -func TestSortitionNonStrictMode(t *testing.T) { - td := setup(t) - exe1 := NewSortitionExecutor(true) - exe2 := NewSortitionExecutor(false) + updatedVal := td.sandbox.Validator(valAddr) - val := td.sandbox.TestStore.RandomTestVal() - lockTime := td.sandbox.CurrentHeight() - proof := td.RandProof() + assert.Equal(t, lockTime, updatedVal.LastSortitionHeight()) + assert.True(t, td.sandbox.IsJoinedCommittee(val.Address())) - td.sandbox.TestAcceptSortition = true - trx := tx.NewSortitionTx(lockTime, val.Address(), proof) - err := exe1.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) - err = exe2.Execute(trx, td.sandbox) - assert.NoError(t, err) + td.checkTotalCoin(t, 0) } func TestChangePower1(t *testing.T) { td := setup(t) - exe := NewSortitionExecutor(true) - // This moves proposer to next validator updateCommittee(td) @@ -141,30 +141,30 @@ func TestChangePower1(t *testing.T) { td.sandbox.UpdateValidator(val2) lockTime := td.sandbox.CurrentHeight() proof2 := td.RandProof() - val3 := td.sandbox.Committee().Validators()[0] + + val3 := td.sandbox.Committee().Proposer(0) proof3 := td.RandProof() td.sandbox.TestParams.CommitteeSize = 4 td.sandbox.TestAcceptSortition = true trx1 := tx.NewSortitionTx(lockTime, val1.Address(), proof1) - err := exe.Execute(trx1, td.sandbox) - assert.NoError(t, err) + td.check(t, trx1, true, nil) + td.check(t, trx1, false, nil) + td.execute(t, trx1) trx2 := tx.NewSortitionTx(lockTime, val2.Address(), proof2) - err = exe.Execute(trx2, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx, "More than 1/3 of power is joining at the same height") + td.check(t, trx2, true, ErrCommitteeJoinLimitExceeded) + td.check(t, trx2, false, nil) // Val3 is a Committee member trx3 := tx.NewSortitionTx(lockTime, val3.Address(), proof3) - err = exe.Execute(trx3, td.sandbox) - assert.NoError(t, err) + td.check(t, trx3, true, nil) + td.check(t, trx3, false, nil) } func TestChangePower2(t *testing.T) { td := setup(t) - exe := NewSortitionExecutor(true) - // This moves proposer to next validator updateCommittee(td) @@ -190,27 +190,31 @@ func TestChangePower2(t *testing.T) { td.sandbox.UpdateValidator(val3) lockTime := td.sandbox.CurrentHeight() proof3 := td.RandProof() - val4 := td.sandbox.Committee().Validators()[0] + + val4 := td.sandbox.Committee().Proposer(0) proof4 := td.RandProof() td.sandbox.TestParams.CommitteeSize = 7 td.sandbox.TestAcceptSortition = true trx1 := tx.NewSortitionTx(lockTime, val1.Address(), proof1) - err := exe.Execute(trx1, td.sandbox) - assert.NoError(t, err) + td.check(t, trx1, true, nil) + td.check(t, trx1, false, nil) + td.execute(t, trx1) trx2 := tx.NewSortitionTx(lockTime, val2.Address(), proof2) - err = exe.Execute(trx2, td.sandbox) - assert.NoError(t, err) + td.check(t, trx2, true, nil) + td.check(t, trx2, false, nil) + td.execute(t, trx2) trx3 := tx.NewSortitionTx(lockTime, val3.Address(), proof3) - err = exe.Execute(trx3, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx, "More than 1/3 of power is leaving at the same height") + td.check(t, trx3, true, ErrCommitteeLeaveLimitExceeded) + td.check(t, trx3, false, nil) // Committee member trx4 := tx.NewSortitionTx(lockTime, val4.Address(), proof4) - err = exe.Execute(trx4, td.sandbox) - assert.NoError(t, err) + td.check(t, trx4, true, nil) + td.check(t, trx4, false, nil) + td.execute(t, trx4) } // TestOldestDidNotPropose tests if the oldest validator in the committee had @@ -218,8 +222,6 @@ func TestChangePower2(t *testing.T) { func TestOldestDidNotPropose(t *testing.T) { td := setup(t) - exe := NewSortitionExecutor(true) - // Let's create validators first vals := make([]*validator.Validator, 9) for i := 0; i < 9; i++ { @@ -245,10 +247,11 @@ func TestOldestDidNotPropose(t *testing.T) { _ = td.sandbox.TestStore.AddTestBlock(height) lockTime := height - trx1 := tx.NewSortitionTx(lockTime, - vals[i].Address(), td.RandProof()) - err := exe.Execute(trx1, td.sandbox) - assert.NoError(t, err) + trx := tx.NewSortitionTx(lockTime, vals[i].Address(), td.RandProof()) + + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) updateCommittee(td) } @@ -258,18 +261,22 @@ func TestOldestDidNotPropose(t *testing.T) { lockTime := td.sandbox.CurrentHeight() trx1 := tx.NewSortitionTx(lockTime, vals[7].Address(), td.RandProof()) - err := exe.Execute(trx1, td.sandbox) - assert.NoError(t, err) + td.check(t, trx1, true, nil) + td.check(t, trx1, false, nil) + td.execute(t, trx1) trx2 := tx.NewSortitionTx(lockTime, vals[8].Address(), td.RandProof()) - err = exe.Execute(trx2, td.sandbox) - assert.NoError(t, err) + td.check(t, trx2, true, nil) + td.check(t, trx2, false, nil) + td.execute(t, trx2) + updateCommittee(td) height++ _ = td.sandbox.TestStore.AddTestBlock(height) // Entering validator 16 - trx3 := tx.NewSortitionTx(lockTime, vals[8].Address(), td.RandProof()) - err = exe.Execute(trx3, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) + trx3 := tx.NewSortitionTx(lockTime+1, vals[8].Address(), td.RandProof()) + td.check(t, trx3, true, ErrOldestValidatorNotProposed) + td.check(t, trx3, false, nil) + td.execute(t, trx3) } diff --git a/execution/executor/transfer.go b/execution/executor/transfer.go index 1b84d0b66..fc733f7f1 100644 --- a/execution/executor/transfer.go +++ b/execution/executor/transfer.go @@ -3,46 +3,58 @@ package executor import ( "github.com/pactus-project/pactus/sandbox" "github.com/pactus-project/pactus/types/account" + "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" - "github.com/pactus-project/pactus/util/errors" ) type TransferExecutor struct { - strict bool + sb sandbox.Sandbox + pld *payload.TransferPayload + fee amount.Amount + sender *account.Account + receiver *account.Account } -func NewTransferExecutor(strict bool) *TransferExecutor { - return &TransferExecutor{strict: strict} -} - -func (*TransferExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func newTransferExecutor(trx *tx.Tx, sb sandbox.Sandbox) (*TransferExecutor, error) { pld := trx.Payload().(*payload.TransferPayload) - senderAcc := sb.Account(pld.From) - if senderAcc == nil { - return errors.Errorf(errors.ErrInvalidAddress, - "unable to retrieve sender account") + sender := sb.Account(pld.From) + if sender == nil { + return nil, AccountNotFoundError{Address: pld.From} } - var receiverAcc *account.Account + + var receiver *account.Account if pld.To == pld.From { - receiverAcc = senderAcc + receiver = sender } else { - receiverAcc = sb.Account(pld.To) - if receiverAcc == nil { - receiverAcc = sb.MakeNewAccount(pld.To) + receiver = sb.Account(pld.To) + if receiver == nil { + receiver = sb.MakeNewAccount(pld.To) } } - if senderAcc.Balance() < pld.Amount+trx.Fee() { + return &TransferExecutor{ + sb: sb, + pld: pld, + fee: trx.Fee(), + sender: sender, + receiver: receiver, + }, nil +} + +func (e *TransferExecutor) Check(_ bool) error { + if e.sender.Balance() < e.pld.Amount+e.fee { return ErrInsufficientFunds } - senderAcc.SubtractFromBalance(pld.Amount + trx.Fee()) - receiverAcc.AddToBalance(pld.Amount) + return nil +} - sb.UpdateAccount(pld.From, senderAcc) - sb.UpdateAccount(pld.To, receiverAcc) +func (e *TransferExecutor) Execute() { + e.sender.SubtractFromBalance(e.pld.Amount + e.fee) + e.receiver.AddToBalance(e.pld.Amount) - return nil + e.sb.UpdateAccount(e.pld.From, e.sender) + e.sb.UpdateAccount(e.pld.To, e.receiver) } diff --git a/execution/executor/transfer_test.go b/execution/executor/transfer_test.go index 2ec5bda96..1cf6f7093 100644 --- a/execution/executor/transfer_test.go +++ b/execution/executor/transfer_test.go @@ -3,93 +3,58 @@ package executor import ( "testing" - "github.com/pactus-project/pactus/sandbox" - "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/util/errors" - "github.com/pactus-project/pactus/util/testsuite" "github.com/stretchr/testify/assert" ) -type testData struct { - *testsuite.TestSuite - - sandbox *sandbox.MockSandbox -} - -func setup(t *testing.T) *testData { - t.Helper() - - ts := testsuite.NewTestSuite(t) - - sb := sandbox.MockingSandbox(ts) - randHeight := ts.RandHeight() - _ = sb.TestStore.AddTestBlock(randHeight) - - return &testData{ - TestSuite: ts, - sandbox: sb, - } -} - -func (td *testData) checkTotalCoin(t *testing.T, fee amount.Amount) { - t.Helper() - - total := amount.Amount(0) - for _, acc := range td.sandbox.TestStore.Accounts { - total += acc.Balance() - } - - for _, val := range td.sandbox.TestStore.Validators { - total += val.Stake() - } - assert.Equal(t, total+fee, amount.Amount(21_000_000*1e9)) -} - func TestExecuteTransferTx(t *testing.T) { td := setup(t) - exe := NewTransferExecutor(true) senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc() senderBalance := senderAcc.Balance() receiverAddr := td.RandAccAddress() + amt := td.RandAmountRange(0, senderBalance) fee := td.RandFee() lockTime := td.sandbox.CurrentHeight() - t.Run("Should fail, Sender has no account", func(t *testing.T) { - trx := tx.NewTransferTx(lockTime, td.RandAccAddress(), - receiverAddr, amt, fee, "non-existing account") + t.Run("Should fail, unknown address", func(t *testing.T) { + randomAddr := td.RandAccAddress() + trx := tx.NewTransferTx(lockTime, randomAddr, + receiverAddr, amt, fee, "unknown address") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) + td.check(t, trx, true, AccountNotFoundError{Address: randomAddr}) + td.check(t, trx, false, AccountNotFoundError{Address: randomAddr}) }) t.Run("Should fail, insufficient balance", func(t *testing.T) { trx := tx.NewTransferTx(lockTime, senderAddr, receiverAddr, senderBalance+1, 0, "insufficient balance") - err := exe.Execute(trx, td.sandbox) - assert.ErrorIs(t, err, ErrInsufficientFunds) + td.check(t, trx, true, ErrInsufficientFunds) + td.check(t, trx, false, ErrInsufficientFunds) }) t.Run("Ok", func(t *testing.T) { trx := tx.NewTransferTx(lockTime, senderAddr, receiverAddr, amt, fee, "ok") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) }) - assert.Equal(t, td.sandbox.Account(senderAddr).Balance(), senderBalance-(amt+fee)) - assert.Equal(t, td.sandbox.Account(receiverAddr).Balance(), amt) + updatedSenderAcc := td.sandbox.Account(senderAddr) + updatedReceiverAcc := td.sandbox.Account(receiverAddr) + + assert.Equal(t, senderBalance-(amt+fee), updatedSenderAcc.Balance()) + assert.Equal(t, amt, updatedReceiverAcc.Balance()) td.checkTotalCoin(t, fee) } func TestTransferToSelf(t *testing.T) { td := setup(t) - exe := NewTransferExecutor(true) senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc() amt := td.RandAmountRange(0, senderAcc.Balance()) @@ -97,9 +62,13 @@ func TestTransferToSelf(t *testing.T) { lockTime := td.sandbox.CurrentHeight() trx := tx.NewTransferTx(lockTime, senderAddr, senderAddr, amt, fee, "ok") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) expectedBalance := senderAcc.Balance() - fee // Fee should be deducted - assert.Equal(t, expectedBalance, td.sandbox.Account(senderAddr).Balance()) + updatedAcc := td.sandbox.Account(senderAddr) + assert.Equal(t, expectedBalance, updatedAcc.Balance()) + + td.checkTotalCoin(t, fee) } diff --git a/execution/executor/unbond.go b/execution/executor/unbond.go index f13702e1d..ed6f6b8cb 100644 --- a/execution/executor/unbond.go +++ b/execution/executor/unbond.go @@ -4,58 +4,62 @@ import ( "github.com/pactus-project/pactus/sandbox" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" - "github.com/pactus-project/pactus/util/errors" + "github.com/pactus-project/pactus/types/validator" ) type UnbondExecutor struct { - strict bool + sb sandbox.Sandbox + pld *payload.UnbondPayload + validator *validator.Validator } -func NewUnbondExecutor(strict bool) *UnbondExecutor { - return &UnbondExecutor{strict: strict} -} - -func (e *UnbondExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func newUnbondExecutor(trx *tx.Tx, sb sandbox.Sandbox) (*UnbondExecutor, error) { pld := trx.Payload().(*payload.UnbondPayload) val := sb.Validator(pld.Signer()) if val == nil { - return errors.Errorf(errors.ErrInvalidAddress, - "unable to retrieve validator") + return nil, ValidatorNotFoundError{Address: pld.Validator} } - if val.UnbondingHeight() > 0 { - return errors.Errorf(errors.ErrInvalidHeight, - "validator has unbonded at height %v", val.UnbondingHeight()) + return &UnbondExecutor{ + sb: sb, + pld: pld, + validator: val, + }, nil +} + +func (e *UnbondExecutor) Check(strict bool) error { + if e.validator.UnbondingHeight() > 0 { + return ErrValidatorUnbonded } - if e.strict { + + if strict { // In strict mode, the unbond transaction will be rejected if the // validator is in the committee. // In non-strict mode, they are added to the transaction pool and // processed once eligible. - if sb.Committee().Contains(pld.Validator) { - return errors.Errorf(errors.ErrInvalidTx, - "validator %v is in committee", pld.Validator) + if e.sb.Committee().Contains(e.pld.Validator) { + return ErrValidatorInCommittee } // In strict mode, unbond transactions will be rejected if a validator is // going to be in the committee for the next height. // In non-strict mode, they are added to the transaction pool and // processed once eligible. - if sb.IsJoinedCommittee(pld.Validator) { - return errors.Errorf(errors.ErrInvalidHeight, - "validator %v joins committee in the next height", pld.Validator) + if e.sb.IsJoinedCommittee(e.pld.Validator) { + return ErrValidatorInCommittee } } - unbondedPower := val.Power() - val.UpdateUnbondingHeight(sb.CurrentHeight()) + return nil +} - // At this point, the validator's power is zero. - // However, we know the validator's stake. - // So, we can update the power delta with the negative of the validator's stake. - sb.UpdatePowerDelta(-1 * unbondedPower) - sb.UpdateValidator(val) +func (e *UnbondExecutor) Execute() { + unbondedPower := e.validator.Power() + e.validator.UpdateUnbondingHeight(e.sb.CurrentHeight()) - return nil + // The validator's power is reduced to zero, + // so we update the power delta with the negative value of the validator's power. + e.sb.UpdatePowerDelta(-1 * unbondedPower) + e.sb.UpdateValidator(e.validator) } diff --git a/execution/executor/unbond_test.go b/execution/executor/unbond_test.go index b69efdb83..d01c3d4df 100644 --- a/execution/executor/unbond_test.go +++ b/execution/executor/unbond_test.go @@ -4,13 +4,11 @@ import ( "testing" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/util/errors" "github.com/stretchr/testify/assert" ) func TestExecuteUnbondTx(t *testing.T) { td := setup(t) - exe := NewUnbondExecutor(true) bonderAddr, bonderAcc := td.sandbox.TestStore.RandomTestAcc() bonderBalance := bonderAcc.Balance() @@ -27,90 +25,60 @@ func TestExecuteUnbondTx(t *testing.T) { td.sandbox.UpdateValidator(val) lockTime := td.sandbox.CurrentHeight() - t.Run("Should fail, Invalid validator", func(t *testing.T) { - trx := tx.NewUnbondTx(lockTime, td.RandAccAddress(), "invalid validator") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) + t.Run("Should fail, unknown address", func(t *testing.T) { + randomAddr := td.RandValAddress() + trx := tx.NewUnbondTx(lockTime, randomAddr, "unknown address") + + td.check(t, trx, true, ValidatorNotFoundError{Address: randomAddr}) + td.check(t, trx, false, ValidatorNotFoundError{Address: randomAddr}) }) - t.Run("Should fail, Inside committee", func(t *testing.T) { + t.Run("Should fail, inside committee", func(t *testing.T) { val0 := td.sandbox.Committee().Proposer(0) trx := tx.NewUnbondTx(lockTime, val0.Address(), "inside committee") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidTx) + + td.check(t, trx, true, ErrValidatorInCommittee) + td.check(t, trx, false, nil) }) - t.Run("Should fail, Cannot unbond if unbonded already", func(t *testing.T) { - pub, _ := td.RandBLSKeyPair() - unbondedVal := td.sandbox.MakeNewValidator(pub) - unbondedVal.UpdateUnbondingHeight(td.sandbox.CurrentHeight()) - td.sandbox.UpdateValidator(unbondedVal) + t.Run("Should fail, joining committee", func(t *testing.T) { + randPub, _ := td.RandBLSKeyPair() + randVal := td.sandbox.MakeNewValidator(randPub) + td.sandbox.UpdateValidator(randVal) + td.sandbox.JoinedToCommittee(randVal.Address()) + trx := tx.NewUnbondTx(lockTime, randPub.ValidatorAddress(), "joining committee") - trx := tx.NewUnbondTx(lockTime, pub.ValidatorAddress(), "Ok") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidHeight) + td.check(t, trx, true, ErrValidatorInCommittee) + td.check(t, trx, false, nil) }) t.Run("Ok", func(t *testing.T) { trx := tx.NewUnbondTx(lockTime, valAddr, "Ok") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) - - // Execute again, should fail - err = exe.Execute(trx, td.sandbox) - assert.Error(t, err) + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) }) - assert.Equal(t, stake, td.sandbox.Validator(valAddr).Stake()) - assert.Zero(t, td.sandbox.Validator(valAddr).Power()) - assert.Equal(t, td.sandbox.Validator(valAddr).UnbondingHeight(), td.sandbox.CurrentHeight()) - assert.Equal(t, int64(-val.Stake()), td.sandbox.PowerDelta()) - - td.checkTotalCoin(t, 0) -} - -// TestUnbondInsideCommittee checks if a validator inside the committee tries to -// unbond the stake. -// In non-strict mode it should be accepted. -func TestUnbondInsideCommittee(t *testing.T) { - td := setup(t) - - exe1 := NewUnbondExecutor(true) - exe2 := NewUnbondExecutor(false) - lockTime := td.sandbox.CurrentHeight() - - val := td.sandbox.Committee().Proposer(0) - trx := tx.NewUnbondTx(lockTime, val.Address(), "") + t.Run("Should fail, Cannot unbond if already unbonded", func(t *testing.T) { + trx := tx.NewUnbondTx(lockTime, valAddr, "Ok") - assert.Error(t, exe1.Execute(trx, td.sandbox)) - assert.NoError(t, exe2.Execute(trx, td.sandbox)) -} + td.check(t, trx, true, ErrValidatorUnbonded) + td.check(t, trx, false, ErrValidatorUnbonded) + }) -// TestUnbondJoiningCommittee checks if a validator tries to unbond after -// evaluating sortition. -// In non-strict mode it should be accepted. -func TestUnbondJoiningCommittee(t *testing.T) { - td := setup(t) - exe1 := NewUnbondExecutor(true) - exe2 := NewUnbondExecutor(false) - pub, _ := td.RandBLSKeyPair() + updatedVal := td.sandbox.Validator(valAddr) - curHeight := td.sandbox.CurrentHeight() - val := td.sandbox.MakeNewValidator(pub) - val.UpdateLastSortitionHeight(curHeight) - td.sandbox.UpdateValidator(val) - td.sandbox.JoinedToCommittee(val.Address()) - lockTime := td.sandbox.CurrentHeight() + assert.Equal(t, stake, updatedVal.Stake()) + assert.Zero(t, updatedVal.Power()) + assert.Equal(t, lockTime, updatedVal.UnbondingHeight()) + assert.Equal(t, int64(-stake), td.sandbox.PowerDelta()) - trx := tx.NewUnbondTx(lockTime, pub.ValidatorAddress(), "Ok") - assert.Error(t, exe1.Execute(trx, td.sandbox)) - assert.NoError(t, exe2.Execute(trx, td.sandbox)) + td.checkTotalCoin(t, 0) } func TestPowerDeltaUnbond(t *testing.T) { td := setup(t) - exe := NewUnbondExecutor(true) pub, _ := td.RandBLSKeyPair() valAddr := pub.ValidatorAddress() @@ -121,8 +89,7 @@ func TestPowerDeltaUnbond(t *testing.T) { lockTime := td.sandbox.CurrentHeight() trx := tx.NewUnbondTx(lockTime, valAddr, "Ok") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) + td.execute(t, trx) assert.Equal(t, int64(-amt), td.sandbox.PowerDelta()) } diff --git a/execution/executor/withdraw.go b/execution/executor/withdraw.go index 3c888928c..eb3397d2d 100644 --- a/execution/executor/withdraw.go +++ b/execution/executor/withdraw.go @@ -2,51 +2,63 @@ package executor import ( "github.com/pactus-project/pactus/sandbox" + "github.com/pactus-project/pactus/types/account" + "github.com/pactus-project/pactus/types/amount" "github.com/pactus-project/pactus/types/tx" "github.com/pactus-project/pactus/types/tx/payload" - "github.com/pactus-project/pactus/util/errors" + "github.com/pactus-project/pactus/types/validator" ) type WithdrawExecutor struct { - strict bool + sb sandbox.Sandbox + pld *payload.WithdrawPayload + fee amount.Amount + sender *validator.Validator + receiver *account.Account } -func NewWithdrawExecutor(strict bool) *WithdrawExecutor { - return &WithdrawExecutor{strict: strict} -} - -func (*WithdrawExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error { +func newWithdrawExecutor(trx *tx.Tx, sb sandbox.Sandbox) (*WithdrawExecutor, error) { pld := trx.Payload().(*payload.WithdrawPayload) - val := sb.Validator(pld.From) - if val == nil { - return errors.Errorf(errors.ErrInvalidAddress, - "unable to retrieve validator account") + sender := sb.Validator(pld.From) + if sender == nil { + return nil, ValidatorNotFoundError{Address: pld.From} } - if val.Stake() < pld.Amount+trx.Fee() { - return ErrInsufficientFunds + receiver := sb.Account(pld.To) + if receiver == nil { + receiver = sb.MakeNewAccount(pld.To) } - if val.UnbondingHeight() == 0 { - return errors.Errorf(errors.ErrInvalidHeight, - "need to unbond first") + + return &WithdrawExecutor{ + sb: sb, + pld: pld, + fee: trx.Fee(), + sender: sender, + receiver: receiver, + }, nil +} + +func (e *WithdrawExecutor) Check(_ bool) error { + if e.sender.Stake() < e.pld.Amount+e.fee { + return ErrInsufficientFunds } - if sb.CurrentHeight() < val.UnbondingHeight()+sb.Params().UnbondInterval { - return errors.Errorf(errors.ErrInvalidHeight, - "hasn't passed unbonding period, expected: %v, got: %v", - val.UnbondingHeight()+sb.Params().UnbondInterval, sb.CurrentHeight()) + + if e.sender.UnbondingHeight() == 0 { + return ErrValidatorBonded } - acc := sb.Account(pld.To) - if acc == nil { - acc = sb.MakeNewAccount(pld.To) + if e.sb.CurrentHeight() < e.sender.UnbondingHeight()+e.sb.Params().UnbondInterval { + return ErrUnbondingPeriod } - val.SubtractFromStake(pld.Amount + trx.Fee()) - acc.AddToBalance(pld.Amount) + return nil +} - sb.UpdateValidator(val) - sb.UpdateAccount(pld.To, acc) +func (e *WithdrawExecutor) Execute() { + e.sender.SubtractFromStake(e.pld.Amount + e.fee) + e.receiver.AddToBalance(e.pld.Amount) - return nil + e.sb.UpdateValidator(e.sender) + e.sb.UpdateAccount(e.pld.To, e.receiver) } diff --git a/execution/executor/withdraw_test.go b/execution/executor/withdraw_test.go index b92cc3ce2..f9498e005 100644 --- a/execution/executor/withdraw_test.go +++ b/execution/executor/withdraw_test.go @@ -4,86 +4,85 @@ import ( "testing" "github.com/pactus-project/pactus/types/tx" - "github.com/pactus-project/pactus/util/errors" "github.com/stretchr/testify/assert" ) func TestExecuteWithdrawTx(t *testing.T) { td := setup(t) - exe := NewWithdrawExecutor(true) - addr := td.RandAccAddress() - pub, _ := td.RandBLSKeyPair() - val := td.sandbox.MakeNewValidator(pub) - accAddr, acc := td.sandbox.TestStore.RandomTestAcc() - amt := td.RandAmountRange(0, acc.Balance()) - fee := td.RandFee() - val.AddToStake(amt + fee) - acc.SubtractFromBalance(amt + fee) - td.sandbox.UpdateAccount(accAddr, acc) + bonderAddr, bonderAcc := td.sandbox.TestStore.RandomTestAcc() + bonderBalance := bonderAcc.Balance() + stake := td.RandAmountRange( + td.sandbox.TestParams.MinimumStake, + bonderBalance) + bonderAcc.SubtractFromBalance(stake) + td.sandbox.UpdateAccount(bonderAddr, bonderAcc) + + valPub, _ := td.RandBLSKeyPair() + val := td.sandbox.MakeNewValidator(valPub) + val.AddToStake(stake) td.sandbox.UpdateValidator(val) - lockTime := td.sandbox.CurrentHeight() - t.Run("Should fail, Invalid validator", func(t *testing.T) { - trx := tx.NewWithdrawTx(lockTime, td.RandAccAddress(), addr, - amt, fee, "invalid validator") - - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidAddress) - }) + totalStake := val.Stake() + fee := td.RandFee() + amt := td.RandAmountRange(0, totalStake-fee) + senderAddr := val.Address() + receiverAddr := td.RandAccAddress() + lockTime := td.sandbox.CurrentHeight() - t.Run("Should fail, insufficient balance", func(t *testing.T) { - trx := tx.NewWithdrawTx(lockTime, val.Address(), addr, - amt+1, fee, "insufficient balance") + t.Run("Should fail, unknown address", func(t *testing.T) { + randomAddr := td.RandValAddress() + trx := tx.NewWithdrawTx(lockTime, randomAddr, receiverAddr, + amt, fee, "unknown address") - err := exe.Execute(trx, td.sandbox) - assert.ErrorIs(t, err, ErrInsufficientFunds) + td.check(t, trx, true, ValidatorNotFoundError{Address: randomAddr}) + td.check(t, trx, false, ValidatorNotFoundError{Address: randomAddr}) }) t.Run("Should fail, hasn't unbonded yet", func(t *testing.T) { - assert.Zero(t, val.UnbondingHeight()) - trx := tx.NewWithdrawTx(lockTime, val.Address(), addr, + trx := tx.NewWithdrawTx(lockTime, senderAddr, receiverAddr, amt, fee, "need to unbond first") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidHeight) + td.check(t, trx, true, ErrValidatorBonded) + td.check(t, trx, false, ErrValidatorBonded) }) val.UpdateUnbondingHeight(td.sandbox.CurrentHeight() - td.sandbox.Params().UnbondInterval + 1) td.sandbox.UpdateValidator(val) - t.Run("Should fail, hasn't passed unbonding interval", func(t *testing.T) { - assert.NotZero(t, val.UnbondingHeight()) - trx := tx.NewWithdrawTx(lockTime, val.Address(), addr, - amt, fee, "not passed unbonding interval") - err := exe.Execute(trx, td.sandbox) - assert.Equal(t, errors.Code(err), errors.ErrInvalidHeight) + t.Run("Should fail, insufficient balance", func(t *testing.T) { + trx := tx.NewWithdrawTx(lockTime, senderAddr, receiverAddr, + totalStake, 1, "insufficient balance") + + td.check(t, trx, true, ErrInsufficientFunds) + td.check(t, trx, false, ErrInsufficientFunds) + }) + + t.Run("Should fail, hasn't passed unbonding period", func(t *testing.T) { + trx := tx.NewWithdrawTx(lockTime, senderAddr, receiverAddr, + amt, fee, "unbonding period") + + td.check(t, trx, true, ErrUnbondingPeriod) + td.check(t, trx, false, ErrUnbondingPeriod) }) curHeight := td.sandbox.CurrentHeight() td.sandbox.TestStore.AddTestBlock(curHeight + 1) t.Run("Should pass, Everything is Ok!", func(t *testing.T) { - trx := tx.NewWithdrawTx(lockTime, val.Address(), addr, - amt, fee, "should be able to empty stake") + trx := tx.NewWithdrawTx(lockTime, senderAddr, receiverAddr, + amt, fee, "should be able to withdraw the stake") - err := exe.Execute(trx, td.sandbox) - assert.NoError(t, err) + td.check(t, trx, true, nil) + td.check(t, trx, false, nil) + td.execute(t, trx) }) - t.Run("Should fail, can't withdraw empty stake", func(t *testing.T) { - trx := tx.NewWithdrawTx(lockTime, val.Address(), addr, - 1, fee, "can't withdraw empty stake") - - err := exe.Execute(trx, td.sandbox) - assert.ErrorIs(t, err, ErrInsufficientFunds) - }) + updatedSenderVal := td.sandbox.Validator(senderAddr) + updatedReceiverAcc := td.sandbox.Account(receiverAddr) - assert.Zero(t, td.sandbox.Validator(val.Address()).Stake()) - assert.Equal(t, td.sandbox.Account(addr).Balance(), amt) - assert.Zero(t, td.sandbox.Validator(val.Address()).Stake()) - assert.Zero(t, td.sandbox.Validator(val.Address()).Power()) - assert.Equal(t, td.sandbox.Account(addr).Balance(), amt) + assert.Equal(t, totalStake-amt-fee, updatedSenderVal.Stake()) + assert.Equal(t, amt, updatedReceiverAcc.Balance()) td.checkTotalCoin(t, fee) } diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go index b388f3333..17bf23b16 100644 --- a/sandbox/sandbox_test.go +++ b/sandbox/sandbox_test.go @@ -225,15 +225,13 @@ func TestTotalAccountCounter(t *testing.T) { td := setup(t) t.Run("Should update total account counter", func(t *testing.T) { - assert.Equal(t, td.store.TotalAccounts(), int32(len(td.valKeys)+1)) - - addr1 := td.RandAccAddress() - addr2 := td.RandAccAddress() - acc := td.sandbox.MakeNewAccount(addr1) - assert.Equal(t, acc.Number(), int32(td.sandbox.Committee().Size()+1)) - acc2 := td.sandbox.MakeNewAccount(addr2) - assert.Equal(t, acc2.Number(), int32(td.sandbox.Committee().Size()+2)) - assert.Zero(t, acc2.Balance()) + totalAccs := td.store.TotalAccounts() + + acc1 := td.sandbox.MakeNewAccount(td.RandAccAddress()) + assert.Equal(t, totalAccs, acc1.Number()) + + acc2 := td.sandbox.MakeNewAccount(td.RandAccAddress()) + assert.Equal(t, totalAccs+1, acc2.Number()) }) } @@ -241,20 +239,15 @@ func TestTotalValidatorCounter(t *testing.T) { td := setup(t) t.Run("Should update total validator counter", func(t *testing.T) { - assert.Equal(t, td.store.TotalValidators(), int32(td.sandbox.Committee().Size())) + totalVals := td.store.TotalValidators() pub, _ := td.RandBLSKeyPair() pub2, _ := td.RandBLSKeyPair() val1 := td.sandbox.MakeNewValidator(pub) - val1.UpdateLastBondingHeight(td.sandbox.CurrentHeight()) - assert.Equal(t, val1.Number(), int32(td.sandbox.Committee().Size())) - assert.Equal(t, val1.LastBondingHeight(), td.sandbox.CurrentHeight()) + assert.Equal(t, totalVals, val1.Number()) val2 := td.sandbox.MakeNewValidator(pub2) - val2.UpdateLastBondingHeight(td.sandbox.CurrentHeight() + 1) - assert.Equal(t, val2.Number(), int32(td.sandbox.Committee().Size()+1)) - assert.Equal(t, val2.LastBondingHeight(), td.sandbox.CurrentHeight()+1) - assert.Zero(t, val2.Stake()) + assert.Equal(t, totalVals+1, val2.Number()) }) } diff --git a/state/execution.go b/state/execution.go index 3e6312777..e97942e69 100644 --- a/state/execution.go +++ b/state/execution.go @@ -9,9 +9,7 @@ import ( "github.com/pactus-project/pactus/util/errors" ) -func (st *state) executeBlock(b *block.Block, sb sandbox.Sandbox) error { - exe := execution.NewExecutor() - +func (st *state) executeBlock(b *block.Block, sb sandbox.Sandbox, check bool) error { var subsidyTrx *tx.Tx for i, trx := range b.Transactions() { // The first transaction should be subsidy transaction @@ -27,9 +25,16 @@ func (st *state) executeBlock(b *block.Block, sb sandbox.Sandbox) error { "duplicated subsidy transaction") } - err := exe.Execute(trx, sb) - if err != nil { - return err + if check { + err := execution.CheckAndExecute(trx, sb, true) + if err != nil { + return err + } + } else { + err := execution.Execute(trx, sb) + if err != nil { + return err + } } } diff --git a/state/execution_test.go b/state/execution_test.go index 1613270e9..a45ef36bf 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -76,7 +76,7 @@ func TestExecuteBlock(t *testing.T) { td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.Error(t, td.state.executeBlock(invBlock, sb)) + assert.Error(t, td.state.executeBlock(invBlock, sb, true)) }) t.Run("Has invalid tx", func(t *testing.T) { @@ -88,7 +88,7 @@ func TestExecuteBlock(t *testing.T) { td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.Error(t, td.state.executeBlock(invBlock, sb)) + assert.Error(t, td.state.executeBlock(invBlock, sb, true)) }) t.Run("Subsidy is not first tx", func(t *testing.T) { @@ -100,7 +100,7 @@ func TestExecuteBlock(t *testing.T) { td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.Error(t, td.state.executeBlock(invBlock, sb)) + assert.Error(t, td.state.executeBlock(invBlock, sb, true)) }) t.Run("Has no subsidy", func(t *testing.T) { @@ -111,7 +111,7 @@ func TestExecuteBlock(t *testing.T) { td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.Error(t, td.state.executeBlock(invBlock, sb)) + assert.Error(t, td.state.executeBlock(invBlock, sb, true)) }) t.Run("Two subsidy transactions", func(t *testing.T) { @@ -123,7 +123,7 @@ func TestExecuteBlock(t *testing.T) { td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.Error(t, td.state.executeBlock(invBlock, sb)) + assert.Error(t, td.state.executeBlock(invBlock, sb, true)) }) t.Run("OK", func(t *testing.T) { @@ -134,7 +134,7 @@ func TestExecuteBlock(t *testing.T) { td.state.stateRoot(), td.state.lastInfo.Certificate(), td.state.lastInfo.SortitionSeed(), proposerAddr) sb := td.state.concreteSandbox() - assert.NoError(t, td.state.executeBlock(invBlock, sb)) + assert.NoError(t, td.state.executeBlock(invBlock, sb, true)) // Check if fee is claimed treasury := sb.Account(crypto.TreasuryAddress) diff --git a/state/state.go b/state/state.go index 21d1aa44d..22c2d3f67 100644 --- a/state/state.go +++ b/state/state.go @@ -320,7 +320,6 @@ func (st *state) ProposeBlock(valKey *bls.ValidatorKey, rewardAddr crypto.Addres // Create new sandbox and execute transactions sb := st.concreteSandbox() - exe := execution.NewExecutor() // Re-check all transactions strictly and remove invalid ones txs := st.txPool.PrepareBlockTransactions() @@ -336,7 +335,7 @@ func (st *state) ProposeBlock(valKey *bls.ValidatorKey, rewardAddr crypto.Addres continue } - if err := exe.Execute(txs[i], sb); err != nil { + if err := execution.CheckAndExecute(txs[i], sb, true); err != nil { st.logger.Debug("found invalid transaction", "tx", txs[i], "error", err) txs.Remove(i) i-- @@ -381,7 +380,7 @@ func (st *state) ValidateBlock(blk *block.Block, round int16) error { sb := st.concreteSandbox() - return st.executeBlock(blk, sb) + return st.executeBlock(blk, sb, true) } func (st *state) CommitBlock(blk *block.Block, cert *certificate.BlockCertificate) error { @@ -423,7 +422,7 @@ func (st *state) CommitBlock(blk *block.Block, cert *certificate.BlockCertificat // ----------------------------------- // Execute block sb := st.concreteSandbox() - if err := st.executeBlock(blk, sb); err != nil { + if err := st.executeBlock(blk, sb, false); err != nil { return err } diff --git a/txpool/txpool.go b/txpool/txpool.go index 5ae70b178..e0b725b7c 100644 --- a/txpool/txpool.go +++ b/txpool/txpool.go @@ -20,7 +20,6 @@ type txPool struct { lk sync.RWMutex config *Config - checker *execution.Execution sandbox sandbox.Sandbox pools map[payload.Type]pool broadcastCh chan message.Message @@ -37,7 +36,6 @@ func NewTxPool(conf *Config, broadcastCh chan message.Message) TxPool { pool := &txPool{ config: conf, - checker: execution.NewChecker(), pools: pools, broadcastCh: broadcastCh, } @@ -126,7 +124,7 @@ func (p *txPool) appendTx(trx *tx.Tx) error { } func (p *txPool) checkTx(trx *tx.Tx) error { - if err := p.checker.Execute(trx, p.sandbox); err != nil { + if err := execution.CheckAndExecute(trx, p.sandbox, false); err != nil { p.logger.Debug("invalid transaction", "tx", trx, "error", err) return err diff --git a/txpool/txpool_test.go b/txpool/txpool_test.go index f057145d2..f430e48f5 100644 --- a/txpool/txpool_test.go +++ b/txpool/txpool_test.go @@ -224,7 +224,7 @@ func TestAddSubsidyTransactions(t *testing.T) { err := td.pool.AppendTx(trx1) assert.ErrorIs(t, err, AppendError{ - Err: execution.PastLockTimeError{ + Err: execution.LockTimeExpiredError{ LockTime: randHeight, }, }) diff --git a/util/errors/errors.go b/util/errors/errors.go index f5d973d10..8efa069db 100644 --- a/util/errors/errors.go +++ b/util/errors/errors.go @@ -14,7 +14,6 @@ const ( ErrInvalidPrivateKey ErrInvalidSignature ErrInvalidTx - ErrInvalidProof ErrInvalidHeight ErrInvalidRound ErrInvalidProposal @@ -35,7 +34,6 @@ var messages = map[int]string{ ErrInvalidPrivateKey: "invalid private key", ErrInvalidSignature: "invalid signature", ErrInvalidTx: "invalid transaction", - ErrInvalidProof: "invalid proof", ErrInvalidHeight: "invalid height", ErrInvalidRound: "invalid round", ErrInvalidProposal: "invalid proposal", diff --git a/util/testsuite/testsuite.go b/util/testsuite/testsuite.go index 9403ad12c..b6a599014 100644 --- a/util/testsuite/testsuite.go +++ b/util/testsuite/testsuite.go @@ -156,7 +156,7 @@ func (ts *TestSuite) RandRound() int16 { return ts.RandInt16(10) } -// RandAmount returns a random amount between [0, 100^e9). +// RandAmount returns a random amount between [0, 1000). func (ts *TestSuite) RandAmount() amount.Amount { return ts.RandAmountRange(0, 1000e9) } @@ -168,9 +168,9 @@ func (ts *TestSuite) RandAmountRange(min, max amount.Amount) amount.Amount { return amt + min } -// RandFee returns a random fee between [0.1, 1). +// RandFee returns a random fee between [0, 1). func (ts *TestSuite) RandFee() amount.Amount { - fee := amount.Amount(ts.RandInt64(0.9e9) + 0.1e9) + fee := amount.Amount(ts.RandInt64(1e9)) return fee } @@ -473,10 +473,11 @@ func (ts *TestSuite) GenerateTestProposal(height uint32, round int16) (*proposal } type TransactionMaker struct { - Amount amount.Amount - Fee amount.Amount - PrvKey *bls.PrivateKey - PubKey *bls.PublicKey + LockTime uint32 + Amount amount.Amount + Fee amount.Amount + PrvKey *bls.PrivateKey + PubKey *bls.PublicKey } // NewTransactionMaker creates a new TransactionMaker instance with default values. @@ -484,10 +485,18 @@ func (ts *TestSuite) NewTransactionMaker() *TransactionMaker { pub, prv := ts.RandBLSKeyPair() return &TransactionMaker{ - Amount: ts.RandAmount(), - Fee: ts.RandFee(), - PrvKey: prv, - PubKey: pub, + LockTime: ts.RandHeight(), + Amount: ts.RandAmount(), + Fee: ts.RandFee(), + PrvKey: prv, + PubKey: pub, + } +} + +// TransactionWithLockTime sets lock-time to the transaction. +func TransactionWithLockTime(lockTime uint32) func(tm *TransactionMaker) { + return func(tm *TransactionMaker) { + tm.LockTime = lockTime } } @@ -520,7 +529,7 @@ func (ts *TestSuite) GenerateTestTransferTx(options ...func(tm *TransactionMaker for _, opt := range options { opt(tm) } - trx := tx.NewTransferTx(ts.RandHeight(), tm.PubKey.AccountAddress(), ts.RandAccAddress(), + trx := tx.NewTransferTx(tm.LockTime, tm.PubKey.AccountAddress(), ts.RandAccAddress(), tm.Amount, tm.Fee, "test send-tx") ts.HelperSignTransaction(tm.PrvKey, trx) @@ -534,7 +543,7 @@ func (ts *TestSuite) GenerateTestBondTx(options ...func(tm *TransactionMaker)) * for _, opt := range options { opt(tm) } - trx := tx.NewBondTx(ts.RandHeight(), tm.PubKey.AccountAddress(), ts.RandValAddress(), + trx := tx.NewBondTx(tm.LockTime, tm.PubKey.AccountAddress(), ts.RandValAddress(), nil, tm.Amount, tm.Fee, "test bond-tx") ts.HelperSignTransaction(tm.PrvKey, trx) @@ -549,7 +558,7 @@ func (ts *TestSuite) GenerateTestSortitionTx(options ...func(tm *TransactionMake opt(tm) } proof := ts.RandProof() - trx := tx.NewSortitionTx(ts.RandHeight(), tm.PubKey.ValidatorAddress(), proof) + trx := tx.NewSortitionTx(tm.LockTime, tm.PubKey.ValidatorAddress(), proof) ts.HelperSignTransaction(tm.PrvKey, trx) return trx @@ -562,7 +571,7 @@ func (ts *TestSuite) GenerateTestUnbondTx(options ...func(tm *TransactionMaker)) for _, opt := range options { opt(tm) } - trx := tx.NewUnbondTx(ts.RandHeight(), tm.PubKey.ValidatorAddress(), "test unbond-tx") + trx := tx.NewUnbondTx(tm.LockTime, tm.PubKey.ValidatorAddress(), "test unbond-tx") ts.HelperSignTransaction(tm.PrvKey, trx) return trx @@ -575,7 +584,7 @@ func (ts *TestSuite) GenerateTestWithdrawTx(options ...func(tm *TransactionMaker for _, opt := range options { opt(tm) } - trx := tx.NewWithdrawTx(ts.RandHeight(), tm.PubKey.ValidatorAddress(), ts.RandAccAddress(), + trx := tx.NewWithdrawTx(tm.LockTime, tm.PubKey.ValidatorAddress(), ts.RandAccAddress(), tm.Amount, tm.Fee, "test withdraw-tx") ts.HelperSignTransaction(tm.PrvKey, trx)