Repository: pactus-project/pactus (go)

Issue:
Adding MinimumStake to the consensus parameters
## Describe the problem you'd like to have solved

Adding `MinimumStake` to the consensus parameters prevents creating dummy validators, specially in the Mainnet.
Spammers might create thousands of validators with small stakes. This has no effect on the consensus mechanism or the sortition algorithm.

### Describe the ideal solution

Adding a new field in the [consensus parameters](https://github.com/pactus-project/pactus/blob/main/types/param/param.go) for `MinimumStake`. The default value can set to 1 PAC coin.



Pull request description:
feat!: adding MinimumStake parameter
## Description

This PR adds the `MinimumStake` parameter to the [consensus parameters](https://github.com/pactus-project/pactus/blob/236b5cac9e124a3baea8162478ae01c958e2f607/types/param/param.go#L5) and the default value is set to 1 PAC coin (10^9).
Consequently, all bond transaction will reject if their value is less than the `MinimumStake`.

## Related issue(s)

- Fixes #501

BREAKING CHANGE

Patch:
diff --git a/execution/executor/bond.go b/execution/executor/bond.go
index 51d4f805..e564399a 100644
--- a/execution/executor/bond.go
+++ b/execution/executor/bond.go
@@ -70,6 +70,9 @@ func (e *BondExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error {
 	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)
+	} else if pld.Stake < sb.Params().MinimumStake {
+		return errors.Errorf(errors.ErrInvalidTx,
+			"validator's stake can't be less than %v", sb.Params().MinimumStake)
 	}
 
 	senderAcc.IncSequence()
diff --git a/execution/executor/transfer.go b/execution/executor/transfer.go
index 9c74e81c..86a1353a 100644
--- a/execution/executor/transfer.go
+++ b/execution/executor/transfer.go
@@ -47,13 +47,13 @@ func (e *TransferExecutor) Execute(trx *tx.Tx, sb sandbox.Sandbox) error {
 			receiverAcc = sb.MakeNewAccount(pld.Receiver)
 		}
 	}
-	if senderAcc.Balance() < pld.Amount+trx.Fee() {
-		return errors.Error(errors.ErrInsufficientFunds)
-	}
 	if senderAcc.Sequence()+1 != trx.Sequence() {
 		return errors.Errorf(errors.ErrInvalidSequence,
 			"expected: %v, got: %v", senderAcc.Sequence()+1, trx.Sequence())
 	}
+	if senderAcc.Balance() < pld.Amount+trx.Fee() {
+		return errors.Error(errors.ErrInsufficientFunds)
+	}
 
 	senderAcc.IncSequence()
 	senderAcc.SubtractFromBalance(pld.Amount + trx.Fee())
diff --git a/types/param/param.go b/types/param/param.go
index 36b4be57..704d3964 100644
--- a/types/param/param.go
+++ b/types/param/param.go
@@ -14,7 +14,8 @@ type Params struct {
 	FeeFraction               float64 `cbor:"9,keyasint"`
 	MinimumFee                int64   `cbor:"10,keyasint"`
 	MaximumFee                int64   `cbor:"11,keyasint"`
-	MaximumStake              int64   `cbor:"12,keyasint"`
+	MinimumStake              int64   `cobr:"12,keyasint"`
+	MaximumStake              int64   `cbor:"13,keyasint"`
 }
 
 func DefaultParams() Params {
@@ -30,6 +31,7 @@ func DefaultParams() Params {
 		FeeFraction:               0.0001,
 		MinimumFee:                1000,
 		MaximumFee:                1000000,
+		MinimumStake:              1000000000,
 		MaximumStake:              1000000000000,
 	}
 }


Test patch:
diff --git a/execution/execution_test.go b/execution/execution_test.go
index f1374a7b..5d791123 100644
--- a/execution/execution_test.go
+++ b/execution/execution_test.go
@@ -131,12 +131,12 @@ func TestChecker(t *testing.T) {
 	t.Run("In strict mode transaction should be rejected.", func(t *testing.T) {
 		signer := ts.RandomSigner()
 		acc := sb.MakeNewAccount(signer.Address())
-		acc.AddToBalance(10000)
+		acc.AddToBalance(10000000000)
 		sb.UpdateAccount(signer.Address(), acc)
 		valPub := sb.TestCommitteeSigners[0].PublicKey()
 
 		trx := tx.NewBondTx(block1000.Stamp(), acc.Sequence()+1, signer.Address(),
-			valPub.Address(), nil, 1000, 1000, "")
+			valPub.Address(), nil, 1000000000, 100000, "")
 		signer.SignMsg(trx)
 		assert.Error(t, executor.Execute(trx, sb))
 		assert.NoError(t, checker.Execute(trx, sb))
diff --git a/execution/executor/bond_test.go b/execution/executor/bond_test.go
index 30fc50f2..63b286aa 100644
--- a/execution/executor/bond_test.go
+++ b/execution/executor/bond_test.go
@@ -17,8 +17,7 @@ func TestExecuteBondTx(t *testing.T) {
 	senderBalance := senderAcc.Balance()
 	pub, _ := td.RandomBLSKeyPair()
 	receiverAddr := pub.Address()
-	fee, amt := td.randomAmountAndFee(senderBalance / 2)
-
+	amt, fee := td.randomAmountAndFee(td.sandbox.TestParams.MinimumStake, senderBalance)
 	t.Run("Should fail, invalid sender", func(t *testing.T) {
 		trx := tx.NewBondTx(td.randStamp, 1, td.RandomAddress(),
 			receiverAddr, pub, amt, fee, "invalid sender")
@@ -80,6 +79,14 @@ func TestExecuteBondTx(t *testing.T) {
 		assert.Equal(t, errors.Code(err), errors.ErrInvalidPublicKey)
 	})
 
+	t.Run("Should fail, amount less than MinimumStake", func(t *testing.T) {
+		trx := tx.NewBondTx(td.RandomStamp(), senderAcc.Sequence()+1, senderAddr,
+			receiverAddr, pub, 1000, fee, "less than MinimumStake")
+
+		err := exe.Execute(trx, td.sandbox)
+		assert.Equal(t, errors.ErrInvalidTx, errors.Code(err))
+	})
+
 	t.Run("Ok", func(t *testing.T) {
 		trx := tx.NewBondTx(td.randStamp, senderAcc.Sequence()+1, senderAddr,
 			receiverAddr, pub, amt, fee, "ok")
@@ -116,7 +123,7 @@ func TestBondInsideCommittee(t *testing.T) {
 	exe2 := NewBondExecutor(false)
 	senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc()
 	senderBalance := senderAcc.Balance()
-	fee, amt := td.randomAmountAndFee(senderBalance)
+	amt, fee := td.randomAmountAndFee(0, senderBalance)
 
 	pub := td.sandbox.Committee().Proposer(0).PublicKey()
 	trx := tx.NewBondTx(td.randStamp, senderAcc.Sequence()+1, senderAddr,
@@ -137,7 +144,7 @@ func TestBondJoiningCommittee(t *testing.T) {
 	senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc()
 	senderBalance := senderAcc.Balance()
 	pub, _ := td.RandomBLSKeyPair()
-	fee, amt := td.randomAmountAndFee(senderBalance)
+	amt, fee := td.randomAmountAndFee(0, senderBalance)
 
 	val := td.sandbox.MakeNewValidator(pub)
 	val.UpdateLastSortitionHeight(td.sandbox.CurrentHeight())
diff --git a/execution/executor/sortition_test.go b/execution/executor/sortition_test.go
index 4ce21a73..4bd7e430 100644
--- a/execution/executor/sortition_test.go
+++ b/execution/executor/sortition_test.go
@@ -31,7 +31,7 @@ func TestExecuteSortitionTx(t *testing.T) {
 	pub, _ := td.RandomBLSKeyPair()
 	newVal := td.sandbox.MakeNewValidator(pub)
 	accAddr, acc := td.sandbox.TestStore.RandomTestAcc()
-	amt, fee := td.randomAmountAndFee(acc.Balance())
+	amt, fee := td.randomAmountAndFee(0, acc.Balance())
 	newVal.AddToStake(amt + fee)
 	acc.SubtractFromBalance(amt + fee)
 	td.sandbox.UpdateAccount(accAddr, acc)
diff --git a/execution/executor/transfer_test.go b/execution/executor/transfer_test.go
index 1e1cea52..8279cd6e 100644
--- a/execution/executor/transfer_test.go
+++ b/execution/executor/transfer_test.go
@@ -46,8 +46,12 @@ func (td *testData) checkTotalCoin(t *testing.T, fee int64) {
 	assert.Equal(t, total+fee, int64(21000000*1e9))
 }
 
-func (td *testData) randomAmountAndFee(max int64) (int64, int64) {
-	amt := td.RandInt64(max / 2)
+func (td *testData) randomAmountAndFee(min int64, max int64) (int64, int64) {
+	amt := td.RandInt64NonZero(max)
+	for amt < min {
+		amt = td.RandInt64NonZero(max)
+	}
+
 	fee := int64(float64(amt) * td.sandbox.Params().FeeFraction)
 	return amt, fee
 }
@@ -59,7 +63,7 @@ func TestExecuteTransferTx(t *testing.T) {
 	senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc()
 	senderBalance := senderAcc.Balance()
 	receiverAddr := td.RandomAddress()
-	amt, fee := td.randomAmountAndFee(senderBalance)
+	amt, fee := td.randomAmountAndFee(0, senderBalance)
 
 	t.Run("Should fail, Sender has no account", func(t *testing.T) {
 		trx := tx.NewTransferTx(td.randStamp, 1, td.RandomAddress(),
@@ -109,7 +113,7 @@ func TestTransferToSelf(t *testing.T) {
 
 	senderAddr, senderAcc := td.sandbox.TestStore.RandomTestAcc()
 	senderBalance := senderAcc.Balance()
-	amt, fee := td.randomAmountAndFee(senderBalance)
+	amt, fee := td.randomAmountAndFee(0, senderBalance)
 
 	trx := tx.NewTransferTx(td.randStamp, senderAcc.Sequence()+1, senderAddr, senderAddr, amt, fee, "ok")
 	err := exe.Execute(trx, td.sandbox)
diff --git a/execution/executor/withdraw_test.go b/execution/executor/withdraw_test.go
index 7c5315ac..959c0d87 100644
--- a/execution/executor/withdraw_test.go
+++ b/execution/executor/withdraw_test.go
@@ -16,7 +16,7 @@ func TestExecuteWithdrawTx(t *testing.T) {
 	pub, _ := td.RandomBLSKeyPair()
 	val := td.sandbox.MakeNewValidator(pub)
 	accAddr, acc := td.sandbox.TestStore.RandomTestAcc()
-	amt, fee := td.randomAmountAndFee(acc.Balance())
+	amt, fee := td.randomAmountAndFee(0, acc.Balance())
 	val.AddToStake(amt + fee)
 	acc.SubtractFromBalance(amt + fee)
 	td.sandbox.UpdateAccount(accAddr, acc)
diff --git a/genesis/genesis_test.go b/genesis/genesis_test.go
index 8e27e32f..ec32a64a 100644
--- a/genesis/genesis_test.go
+++ b/genesis/genesis_test.go
@@ -57,8 +57,7 @@ func TestGenesisTestNet(t *testing.T) {
 	genTime, _ := time.Parse("2006-01-02", "2023-05-08")
 	assert.Equal(t, g.GenesisTime(), genTime)
 	assert.Equal(t, g.Params().BondInterval, uint32(120))
-
-	expected, _ := hash.FromString("d5b41d8c2d45a951329512915ae7b2f4a445aeda380a1d40c50b5a5743dfb3fa")
+	expected, _ := hash.FromString("63f32d682050bd03eb832c9d99fc3b48855d7f4193e5c09205be68d6e16546af")
 	assert.Equal(t, g.Hash(), expected)
 }
 
diff --git a/state/execution_test.go b/state/execution_test.go
index cddb08b9..6dd501f9 100644
--- a/state/execution_test.go
+++ b/state/execution_test.go
@@ -13,9 +13,14 @@ import (
 func TestProposeBlock(t *testing.T) {
 	td := setup(t)
 
+	curHeight := uint32(7)
+	for i := uint32(0); i < curHeight; i++ {
+		td.moveToNextHeightForAllStates(t)
+	}
 	b1, c1 := td.makeBlockAndCertificate(t, 0, td.valSigner1, td.valSigner2, td.valSigner3)
-	assert.NoError(t, td.state1.CommitBlock(1, b1, c1))
-	assert.NoError(t, td.state2.CommitBlock(1, b1, c1))
+	assert.NoError(t, td.state1.CommitBlock(curHeight+1, b1, c1))
+	assert.NoError(t, td.state2.CommitBlock(curHeight+1, b1, c1))
+	assert.Equal(t, td.state1.LastBlockHeight(), curHeight+1)
 
 	invSubsidyTx := tx.NewSubsidyTx(td.state1.lastInfo.BlockHash().Stamp(), 1, td.valSigner2.Address(),
 		td.state1.params.BlockReward, "duplicated subsidy transaction")
@@ -27,7 +32,7 @@ func TestProposeBlock(t *testing.T) {
 	trx1 := tx.NewTransferTx(b1.Stamp(), 1, td.valSigner1.Address(), td.valSigner1.Address(), 1, 1000, "")
 	td.valSigner1.SignMsg(trx1)
 
-	trx2 := tx.NewBondTx(b1.Stamp(), 2, td.valSigner1.Address(), pub.Address(), pub, 1000, 1000, "")
+	trx2 := tx.NewBondTx(b1.Stamp(), 2, td.valSigner1.Address(), pub.Address(), pub, 1000000000, 100000, "")
 	td.valSigner1.SignMsg(trx2)
 
 	assert.NoError(t, td.state1.txPool.AppendTx(invTransferTx))
@@ -41,9 +46,9 @@ func TestProposeBlock(t *testing.T) {
 	assert.Equal(t, b2.Header().PrevBlockHash(), b1.Hash())
 	assert.Equal(t, b2.Transactions()[1:], block.Txs{trx1, trx2})
 	assert.True(t, b2.Transactions()[0].IsSubsidyTx())
-	assert.NoError(t, td.state1.CommitBlock(2, b2, c2))
+	assert.NoError(t, td.state1.CommitBlock(curHeight+2, b2, c2))
 
-	assert.Equal(t, td.state1.TotalPower(), int64(1004))
+	assert.Equal(t, td.state1.TotalPower(), int64(1000000004))
 	assert.Equal(t, td.state1.committee.TotalPower(), int64(4))
 }
 
diff --git a/state/state_test.go b/state/state_test.go
index e215821e..9bdd6da9 100644
--- a/state/state_test.go
+++ b/state/state_test.go
@@ -396,10 +396,10 @@ func TestSortition(t *testing.T) {
 	assert.Equal(t, td.state1.CommitteePower(), int64(4))
 
 	height := uint32(1)
-	for ; height <= 11; height++ {
-		if height == 2 {
+	for ; height <= 15; height++ {
+		if height == 6 {
 			trx := tx.NewBondTx(td.state1.lastInfo.BlockHash().Stamp(), 1, td.valSigner1.Address(),
-				pub.Address(), pub, 10000000, 1000, "")
+				pub.Address(), pub, 1000000000, 100000, "")
 			td.valSigner1.SignMsg(trx)
 
 			assert.NoError(t, td.commonTxPool.AppendTx(trx))
@@ -459,7 +459,7 @@ func TestSortition(t *testing.T) {
 	assert.NoError(t, td.state3.CommitBlock(height, b14, c14))
 	assert.NoError(t, td.state4.CommitBlock(height, b14, c14))
 
-	assert.Equal(t, td.state1.CommitteePower(), int64(10000004))
+	assert.Equal(t, td.state1.CommitteePower(), int64(1000000004))
 	assert.Equal(t, td.state1.TotalValidators(), int32(5))
 }
 
diff --git a/tests/main_test.go b/tests/main_test.go
index 21e387e4..e727b54b 100644
--- a/tests/main_test.go
+++ b/tests/main_test.go
@@ -119,6 +119,7 @@ func TestMain(m *testing.M) {
 	vals[2] = validator.NewValidator(tSigners[tNodeIdx3][0].PublicKey().(*bls.PublicKey), 2)
 	vals[3] = validator.NewValidator(tSigners[tNodeIdx4][0].PublicKey().(*bls.PublicKey), 3)
 	params := param.DefaultParams()
+	params.MinimumStake = 1000
 	params.BlockTimeInSecond = 2
 	params.BondInterval = 8
 	params.CommitteeSize = tCommitteeSize
diff --git a/txpool/pool_test.go b/txpool/pool_test.go
index ee8d8372..c03d3177 100644
--- a/txpool/pool_test.go
+++ b/txpool/pool_test.go
@@ -154,7 +154,7 @@ func TestPrepareBlockTransactions(t *testing.T) {
 
 	pub, _ := td.RandomBLSKeyPair()
 	bondTx := tx.NewBondTx(block1000000.Stamp(), acc1.Sequence()+2, acc1Signer.Address(),
-		pub.Address(), pub, 1000, 1000, "bond-tx")
+		pub.Address(), pub, 1000000000, 100000, "bond-tx")
 	acc1Signer.SignMsg(bondTx)
 
 	unbondTx := tx.NewUnbondTx(block1000000.Stamp(), val1.Sequence()+1, val1.Address(), "unbond-tx")