diff --git a/framework/src/modules/pos/commands/stake.ts b/framework/src/modules/pos/commands/stake.ts index 1989c20e7c1..41fb8e1a48a 100644 --- a/framework/src/modules/pos/commands/stake.ts +++ b/framework/src/modules/pos/commands/stake.ts @@ -97,7 +97,7 @@ export class StakeCommand extends BaseCommand { return { status: VerifyStatus.FAIL, error: new ValidationError( - 'Amount should be multiple of 10 * 10^8.', + `Amount should be multiple of ${this._baseStakeAmount}.`, stake.amount.toString(), ), }; diff --git a/framework/test/unit/modules/pos/commands/claim_rewards.spec.ts b/framework/test/unit/modules/pos/commands/claim_rewards.spec.ts index cf39cf8584a..e86489c0c9e 100644 --- a/framework/test/unit/modules/pos/commands/claim_rewards.spec.ts +++ b/framework/test/unit/modules/pos/commands/claim_rewards.spec.ts @@ -26,7 +26,7 @@ import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefix import { createFakeBlockHeader, InMemoryPrefixedStateDB } from '../../../../../src/testing'; import { createStoreGetter } from '../../../../../src/testing/utils'; -describe('Change Commission command', () => { +describe('Claim Rewards command', () => { const pos = new PoSModule(); const publicKey = utils.getRandomBytes(32); const senderAddress = address.getAddressFromPublicKey(publicKey); diff --git a/framework/test/unit/modules/pos/commands/register_validator.spec.ts b/framework/test/unit/modules/pos/commands/register_validator.spec.ts index e0f6eee62e3..ca982efa582 100644 --- a/framework/test/unit/modules/pos/commands/register_validator.spec.ts +++ b/framework/test/unit/modules/pos/commands/register_validator.spec.ts @@ -37,6 +37,7 @@ import { VALIDATOR_REGISTRATION_FEE, } from '../../../../../src/modules/pos/constants'; import { ValidatorRegisteredEvent } from '../../../../../src/modules/pos/events/validator_registered'; +import { liskToBeddows } from '../../../../utils/assets'; describe('Validator registration command', () => { const pos = new Modules.PoS.PoSModule(); @@ -53,7 +54,6 @@ describe('Validator registration command', () => { generatorKey: utils.getRandomBytes(32), blsKey: utils.getRandomBytes(48), proofOfPossession: utils.getRandomBytes(96), - validatorRegistrationFee: VALIDATOR_REGISTRATION_FEE, }; const defaultValidatorInfo = { name: transactionParams.name, @@ -77,7 +77,7 @@ describe('Validator registration command', () => { command: 'registerValidator', senderPublicKey: publicKey, nonce: BigInt(0), - fee: BigInt(1000000000), + fee: liskToBeddows(20), params: encodedTransactionParams, signatures: [publicKey], }); @@ -206,7 +206,7 @@ describe('Validator registration command', () => { command: 'registerValidator', senderPublicKey: publicKey, nonce: BigInt(0), - fee: BigInt(100000000), + fee: liskToBeddows(1), params: invalidParams, signatures: [publicKey], }); diff --git a/framework/test/unit/modules/pos/commands/stake.spec.ts b/framework/test/unit/modules/pos/commands/stake.spec.ts index b10eab9535d..f8006a0c72d 100644 --- a/framework/test/unit/modules/pos/commands/stake.spec.ts +++ b/framework/test/unit/modules/pos/commands/stake.spec.ts @@ -18,7 +18,10 @@ import { address, utils } from '@liskhq/lisk-cryptography'; import { validator } from '@liskhq/lisk-validator'; import { StateMachine, Modules } from '../../../../../src'; import { + BASE_STAKE_AMOUNT, defaultConfig, + MAX_NUMBER_PENDING_UNLOCKS, + MAX_NUMBER_SENT_STAKES, MODULE_NAME_POS, PoSEventResult, TOKEN_ID_LENGTH, @@ -67,6 +70,7 @@ describe('StakeCommand', () => { const posTokenID = DEFAULT_LOCAL_ID; const senderPublicKey = utils.getRandomBytes(32); const senderAddress = address.getAddressFromPublicKey(senderPublicKey); + const validatorAddress = utils.getRandomBytes(20); const validatorAddress1 = utils.getRandomBytes(20); const validatorAddress2 = utils.getRandomBytes(20); const validatorAddress3 = utils.getRandomBytes(20); @@ -81,7 +85,7 @@ describe('StakeCommand', () => { let validatorStore: ValidatorStore; let context: any; let transaction: any; - let command: Modules.PoS.StakeCommand; + let stakeCommand: Modules.PoS.StakeCommand; let transactionParamsDecoded: any; let stateStore: PrefixedStateReadWriter; let tokenLockMock: jest.Mock; @@ -120,12 +124,12 @@ describe('StakeCommand', () => { internalMethod = new InternalMethod(pos.stores, pos.events, pos.name); internalMethod.addDependencies(tokenMethod); mockAssignStakeRewards = jest.spyOn(internalMethod, 'assignStakeRewards').mockResolvedValue(); - command = new Modules.PoS.StakeCommand(pos.stores, pos.events); - command.addDependencies({ + stakeCommand = new Modules.PoS.StakeCommand(pos.stores, pos.events); + stakeCommand.addDependencies({ tokenMethod, internalMethod, }); - command.init({ + stakeCommand.init({ posTokenID: DEFAULT_LOCAL_ID, factorSelfStakes: defaultConfig.maxNumberSentStakes, baseStakeAmount: BigInt(defaultConfig.baseStakeAmount), @@ -183,31 +187,33 @@ describe('StakeCommand', () => { describe('constructor', () => { it('should have valid name', () => { - expect(command.name).toBe('stake'); + expect(stakeCommand.name).toBe('stake'); }); it('should have valid schema', () => { - expect(command.schema).toMatchSnapshot(); + expect(stakeCommand.schema).toMatchSnapshot(); }); }); describe('verify schema', () => { it('should return errors when transaction.params.stakes does not include any stake', () => { expect(() => - validator.validate(command.schema, { + validator.validate(stakeCommand.schema, { stakes: [], }), ).toThrow('must NOT have fewer than 1 items'); }); - it('should return errors when transaction.params.stakes includes more than 20 elements', () => { + it(`should return errors when transaction.params.stakes includes more than ${ + 2 * MAX_NUMBER_SENT_STAKES + } elements`, () => { expect(() => - validator.validate(command.schema, { - stakes: Array(21) + validator.validate(stakeCommand.schema, { + stakes: Array(2 * MAX_NUMBER_SENT_STAKES + 1) .fill(0) .map(() => ({ - validatorAddress: utils.getRandomBytes(20), - amount: liskToBeddows(0), + validatorAddress, + amount: liskToBeddows(8), })), }), ).toThrow('must NOT have more than 20 items'); @@ -215,7 +221,7 @@ describe('StakeCommand', () => { it('should return errors when transaction.params.stakes includes invalid address', () => { expect(() => - validator.validate(command.schema, { + validator.validate(stakeCommand.schema, { stakes: Array(20) .fill(0) .map(() => ({ @@ -228,10 +234,10 @@ describe('StakeCommand', () => { it('should return errors when transaction.params.stakes includes amount which is less than sint64 range', () => { expect(() => - validator.validate(command.schema, { + validator.validate(stakeCommand.schema, { stakes: [ { - validatorAddress: utils.getRandomBytes(20), + validatorAddress, amount: BigInt(-1) * BigInt(2) ** BigInt(63) - BigInt(1), }, ], @@ -241,10 +247,10 @@ describe('StakeCommand', () => { it('should return errors when transaction.params.stakes includes amount which is greater than sint64 range', () => { expect(() => - validator.validate(command.schema, { + validator.validate(stakeCommand.schema, { stakes: [ { - validatorAddress: utils.getRandomBytes(20), + validatorAddress, amount: BigInt(2) ** BigInt(63) + BigInt(1), }, ], @@ -269,14 +275,16 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes contains valid contents', () => { it('should not throw errors with valid upstake case', async () => { transactionParamsDecoded = { - stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(20) }], + stakes: [{ validatorAddress, amount: liskToBeddows(20) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'status', StateMachine.VerifyStatus.OK, ); @@ -284,14 +292,14 @@ describe('StakeCommand', () => { it('should not throw errors with valid downstake cast', async () => { transactionParamsDecoded = { - stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(-20) }], + stakes: [{ validatorAddress, amount: liskToBeddows(-20) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'status', StateMachine.VerifyStatus.OK, ); @@ -304,92 +312,81 @@ describe('StakeCommand', () => { { validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(-20) }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'status', StateMachine.VerifyStatus.OK, ); }); }); - describe('when transaction.params.stakes contains more than 10 positive stakes', () => { + describe(`when transaction.params.stakes contains more than ${MAX_NUMBER_SENT_STAKES} positive stakes`, () => { it('should throw error', async () => { transactionParamsDecoded = { - stakes: Array(11) + stakes: Array(MAX_NUMBER_SENT_STAKES + 1) .fill(0) .map(() => ({ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(10) })), }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'error.message', - 'Upstake can only be casted up to 10.', + `Upstake can only be casted up to ${MAX_NUMBER_SENT_STAKES}.`, ); }); }); - describe('when transaction.params.stakes contains more than 10 negative stakes', () => { + describe(`when transaction.params.stakes contains more than ${MAX_NUMBER_SENT_STAKES} negative stakes`, () => { it('should throw error', async () => { transactionParamsDecoded = { - stakes: Array(11) + stakes: Array(MAX_NUMBER_SENT_STAKES + 1) .fill(0) .map(() => ({ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(-10), })), }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); - context = createTransactionContext({ - transaction, - }).createCommandVerifyContext(command.schema); - await expect(command.verify(context)).resolves.toHaveProperty( - 'error.message', - 'Downstake can only be casted up to 10.', - ); - }); - }); + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); - describe('when transaction.params.stakes includes duplicate validators within positive amount', () => { - it('should throw error', async () => { - const validatorAddress = utils.getRandomBytes(20); - transactionParamsDecoded = { - stakes: Array(2).fill({ validatorAddress, amount: liskToBeddows(10) }), - }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'error.message', - 'Validator address must be unique.', + 'Downstake can only be casted up to 10.', ); }); }); - describe('when transaction.params.stakes includes duplicate validators within positive and negative amount', () => { + describe('when transaction.params.stakes includes duplicate validators', () => { it('should throw error', async () => { - const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [ { validatorAddress, amount: liskToBeddows(10) }, { validatorAddress, amount: liskToBeddows(-10) }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'error.message', 'Validator address must be unique.', ); @@ -398,72 +395,69 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes zero amount', () => { it('should throw error', async () => { - const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: liskToBeddows(0) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'error.message', 'Amount cannot be 0.', ); }); }); - describe('when transaction.params.stakes includes positive amount which is not multiple of 10 * 10^8', () => { + describe(`when transaction.params.stakes includes an amount which is not a multiple of ${BASE_STAKE_AMOUNT}`, () => { it('should throw an error', async () => { - const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { - stakes: [{ validatorAddress, amount: BigInt(20) }], + stakes: [{ validatorAddress, amount: BASE_STAKE_AMOUNT - BigInt(1) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); - context = createTransactionContext({ - transaction, - }).createCommandVerifyContext(command.schema); - await expect(command.verify(context)).resolves.toHaveProperty( - 'error.message', - 'Amount should be multiple of 10 * 10^8.', - ); - }); - }); + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); - describe('when transaction.params.stakes includes negative amount which is not multiple of 10 * 10^8', () => { - it('should throw error', async () => { - const validatorAddress = utils.getRandomBytes(20); - transactionParamsDecoded = { - stakes: [{ validatorAddress, amount: BigInt(-20) }], - }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, - }).createCommandVerifyContext(command.schema); + }).createCommandVerifyContext(stakeCommand.schema); - await expect(command.verify(context)).resolves.toHaveProperty( + await expect(stakeCommand.verify(context)).resolves.toHaveProperty( 'error.message', - 'Amount should be multiple of 10 * 10^8.', + `Amount should be multiple of ${BASE_STAKE_AMOUNT}.`, ); }); }); }); describe('execute', () => { + beforeEach(() => { + transaction = new Transaction({ + module: 'pos', + command: 'stake', + fee: BigInt(1500000), + nonce: BigInt(0), + params: Buffer.alloc(0), + senderPublicKey, + signatures: [], + }); + }); describe('when transaction.params.stakes contain positive amount', () => { it('should emit ValidatorStakedEvent with STAKE_SUCCESSFUL result', async () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: liskToBeddows(10) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).resolves.toBeUndefined(); + await expect(stakeCommand.execute(context)).resolves.toBeUndefined(); checkEventResult( context.eventQueue, @@ -481,17 +475,19 @@ describe('StakeCommand', () => { it('should throw error if stake amount is more than balance', async () => { transactionParamsDecoded = { - stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(100) }], + stakes: [{ validatorAddress, amount: liskToBeddows(100) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockRejectedValue(new Error('Not enough balance to lock')); - await expect(command.execute(context)).rejects.toThrow(); + await expect(stakeCommand.execute(context)).rejects.toThrow(); }); it('should make account to have correct balance', async () => { @@ -501,13 +497,15 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); expect(tokenLockMock).toHaveBeenCalledTimes(2); expect(tokenLockMock).toHaveBeenCalledWith( @@ -530,13 +528,15 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: validator1StakeAmount }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const { pendingUnlocks } = await stakerStore.get( createStoreGetter(stateStore), @@ -553,13 +553,15 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress1, amount: validator1StakeAmount }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); @@ -576,13 +578,15 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const { totalStake: totalStake1 } = await validatorStore.get( createStoreGetter(stateStore), @@ -627,13 +631,13 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: newStakeAmount }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const { totalStake } = await validatorStore.get( createStoreGetter(stateStore), @@ -649,17 +653,17 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: validator1StakeAmount }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); await expect( stakerStore.get(createStoreGetter(stateStore), senderAddress), ).rejects.toThrow(); - await command.execute(context); + await stakeCommand.execute(context); const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); expect(stakes[0]).toEqual({ validatorAddress: validatorAddress1, @@ -677,16 +681,18 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); transactionParamsDecoded = { stakes: [ @@ -694,20 +700,22 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount * BigInt(-1) }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockClear(); }); it('should emit ValidatorStakedEvent with STAKE_SUCCESSFUL result', async () => { - await expect(command.execute(context)).resolves.toBeUndefined(); + await expect(stakeCommand.execute(context)).resolves.toBeUndefined(); for (let i = 0; i < 2; i += 2) { checkEventResult( @@ -726,7 +734,7 @@ describe('StakeCommand', () => { }); it('should not change account balance', async () => { - await command.execute(context); + await stakeCommand.execute(context); expect(tokenLockMock).toHaveBeenCalledTimes(0); }); @@ -737,13 +745,15 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress1, amount: validator1StakeAmount * BigInt(-1) }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); @@ -757,13 +767,15 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: downStakeAmount * BigInt(-1) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); @@ -776,12 +788,11 @@ describe('StakeCommand', () => { }); }); - it('should make account to have correct unlocking', async () => { - await command.execute(context); + it('should have pending unlocks ordered by validator address', async () => { + await stakeCommand.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - expect(stakerData.pendingUnlocks).toHaveLength(2); expect(stakerData.pendingUnlocks).toEqual( [ { @@ -798,19 +809,8 @@ describe('StakeCommand', () => { ); }); - it('should order stakerData.pendingUnlocks', async () => { - await command.execute(context); - - const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - - expect(stakerData.pendingUnlocks).toHaveLength(2); - expect(stakerData.pendingUnlocks.map((d: any) => d.validatorAddress)).toEqual( - [validatorAddress1, validatorAddress2].sort((a, b) => a.compare(b)), - ); - }); - - it('should make downstaked validator account to have correct totalStake', async () => { - await command.execute(context); + it('should make downstaked validator account to have correct totalStakeReceived', async () => { + await stakeCommand.execute(context); const validatorData1 = await validatorStore.get( createStoreGetter(stateStore), @@ -831,13 +831,15 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress3, amount: downStakeAmount * BigInt(-1) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow( + await expect(stakeCommand.execute(context)).rejects.toThrow( 'Cannot cast downstake to validator who is not upstaked.', ); @@ -866,16 +868,18 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); transactionParamsDecoded = { stakes: [ @@ -883,20 +887,22 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: negativeStakeValidator2 }, ].sort((a, b) => -1 * a.validatorAddress.compare(b.validatorAddress)), }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockClear(); }); it('should assign reward to staker for downstake and upstake for already staked validator', async () => { - await expect(command.execute(context)).resolves.toBeUndefined(); + await expect(stakeCommand.execute(context)).resolves.toBeUndefined(); expect(mockAssignStakeRewards).toHaveBeenCalledTimes(2); }); @@ -919,7 +925,7 @@ describe('StakeCommand', () => { await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validator1); await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validator2); - await expect(command.execute(context)).resolves.toBeUndefined(); + await expect(stakeCommand.execute(context)).resolves.toBeUndefined(); const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); @@ -937,20 +943,21 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress3, amount: positiveStakeValidator1 }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).resolves.toBeUndefined(); + await expect(stakeCommand.execute(context)).resolves.toBeUndefined(); }); it('should update staked validator in EligibleValidatorStore', async () => { - const validatorAddress = utils.getRandomBytes(20); const selfStake = BigInt(2) + BigInt(defaultConfig.minWeightStandby); const val = { @@ -964,16 +971,18 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress, amount: positiveStakeValidator1 }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const eligibleValidatorStore = pos.stores.get(EligibleValidatorsStore); @@ -987,16 +996,18 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress, amount: BigInt(-2) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); expect( await eligibleValidatorStore.get( @@ -1007,7 +1018,7 @@ describe('StakeCommand', () => { }); it('should make staker to have correct balance', async () => { - await command.execute(context); + await stakeCommand.execute(context); expect(tokenLockMock).toHaveBeenCalledTimes(1); expect(tokenLockMock).toHaveBeenCalledWith( @@ -1020,10 +1031,9 @@ describe('StakeCommand', () => { }); it('should make staker to have correct unlocking', async () => { - await command.execute(context); + await stakeCommand.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - expect(stakerData.pendingUnlocks).toHaveLength(1); expect(stakerData.pendingUnlocks).toEqual([ { validatorAddress: validatorAddress2, @@ -1033,8 +1043,8 @@ describe('StakeCommand', () => { ]); }); - it('should make upstaked validator account to have correct totalStake', async () => { - await command.execute(context); + it('should make upstaked validator account to have correct totalStakeReceived', async () => { + await stakeCommand.execute(context); const updatedValidator1 = await validatorStore.get( createStoreGetter(stateStore), @@ -1046,8 +1056,8 @@ describe('StakeCommand', () => { ); }); - it('should make downstaked validator account to have correct totalStake', async () => { - await command.execute(context); + it('should make downstaked validator account to have correct totalStakeReceived', async () => { + await stakeCommand.execute(context); const validatorData2 = await validatorStore.get( createStoreGetter(stateStore), @@ -1066,14 +1076,16 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ].sort((a, b) => -1 * a.validatorAddress.compare(b.validatorAddress)), }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockClear(); }); @@ -1086,16 +1098,18 @@ describe('StakeCommand', () => { ...transactionParamsDecoded, stakes: [{ validatorAddress: nonExistingValidatorAddress, amount: liskToBeddows(76) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow( + await expect(stakeCommand.execute(context)).rejects.toThrow( 'Invalid stake: no registered validator with the specified address', ); @@ -1114,12 +1128,34 @@ describe('StakeCommand', () => { }); }); - describe('when transaction.params.stakes positive amount makes StakerData.stakes array contain more than 10 elements', () => { + describe(`when an account has already staked with ${ + MAX_NUMBER_SENT_STAKES - 2 + } different validators, and then sends a new transaction to stake with 4 more`, () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_SENT_STAKES failure', async () => { + const stakerData = await stakerStore.getOrDefault( + createStoreGetter(stateStore), + senderAddress, + ); + + const existingSentStakesCount = MAX_NUMBER_SENT_STAKES - 2; + + for (let i = 0; i < existingSentStakesCount; i += 1) { + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19, 1), Buffer.alloc(1, i)]); + + stakerData.stakes.push({ + validatorAddress: uniqueValidatorAddress, + amount: liskToBeddows(20), + sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], + }); + } + + await stakerStore.set(createStoreGetter(stateStore), senderAddress, stakerData); + const stakes = []; + const newStakesCount = 4; - for (let i = 0; i < 12; i += 1) { - const validatorAddress = utils.getRandomBytes(20); + for (let i = 0; i < newStakesCount; i += 1) { + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19, 2), Buffer.alloc(1, i)]); const validatorInfo = { consecutiveMissedBlocks: 0, @@ -1136,50 +1172,59 @@ describe('StakeCommand', () => { await validatorStore.set( createStoreGetter(stateStore), - validatorAddress, + uniqueValidatorAddress, validatorInfo, ); stakes.push({ - validatorAddress, + validatorAddress: uniqueValidatorAddress, amount: liskToBeddows(10), }); } transactionParamsDecoded = { stakes }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow('Sender can only stake upto 10.'); + await expect(stakeCommand.execute(context)).rejects.toThrow( + `Sender can only stake upto ${MAX_NUMBER_SENT_STAKES}.`, + ); + + // count events until the first failed one + const totalEventsCount = MAX_NUMBER_SENT_STAKES - existingSentStakesCount + 1; checkEventResult( context.eventQueue, - 11, + totalEventsCount, ValidatorStakedEvent, - 10, + totalEventsCount - 1, { senderAddress, - validatorAddress: transactionParamsDecoded.stakes[10].validatorAddress, - amount: transactionParamsDecoded.stakes[10].amount, + validatorAddress: + transactionParamsDecoded.stakes[totalEventsCount - 1].validatorAddress, + amount: transactionParamsDecoded.stakes[totalEventsCount - 1].amount, }, PoSEventResult.STAKE_FAILED_TOO_MANY_SENT_STAKES, ); }); }); - describe('when transaction.params.stakes negative amount decrease StakerData.stakes array entries, yet positive amount makes account exceeds more than 10', () => { + describe(`when transaction.params.stakes downstakes decrease stakerData.sentStakes entries, yet upstakes make account exceeds more than ${MAX_NUMBER_SENT_STAKES} stakes`, () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_SENT_STAKES failure', async () => { - const initialValidatorAmount = 8; + const amount = liskToBeddows(20); const stakerData = await stakerStore.getOrDefault( createStoreGetter(stateStore), senderAddress, ); - // Suppose account already staked for 8 validators - for (let i = 0; i < initialValidatorAmount; i += 1) { - const validatorAddress = utils.getRandomBytes(20); + // Suppose account only has room for 2 more validators to stake with + const existingSentStakesCount = MAX_NUMBER_SENT_STAKES - 2; + for (let i = 0; i < existingSentStakesCount; i += 1) { + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19, 1), Buffer.alloc(1, i)]); const validatorInfo = { consecutiveMissedBlocks: 0, @@ -1196,13 +1241,13 @@ describe('StakeCommand', () => { await validatorStore.set( createStoreGetter(stateStore), - validatorAddress, + uniqueValidatorAddress, validatorInfo, ); const stake = { - validatorAddress, - amount: liskToBeddows(20), + validatorAddress: uniqueValidatorAddress, + amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }; stakerData.stakes.push(stake); @@ -1210,27 +1255,26 @@ describe('StakeCommand', () => { await stakerStore.set(createStoreGetter(stateStore), senderAddress, stakerData); - // We have 2 negative stakes - const stakes = [ - { - validatorAddress: stakerData.stakes[0].validatorAddress, - amount: liskToBeddows(-10), - }, - { - validatorAddress: stakerData.stakes[1].validatorAddress, - amount: liskToBeddows(-10), - }, - ]; + // We have 2 downstakes + const downstakeCount = 2; + const stakes = []; + + for (let i = 0; i < downstakeCount; i += 1) { + stakes.push({ + validatorAddress: stakerData.stakes[i].validatorAddress, + amount: -amount, + }); + } - // We have 3 positive stakes - for (let i = 0; i < 3; i += 1) { - const validatorAddress = utils.getRandomBytes(20); + // We have 7 upstakes + for (let i = 0; i < 7; i += 1) { + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19, 2), Buffer.alloc(1, i)]); const validatorInfo = { consecutiveMissedBlocks: 0, isBanned: false, lastGeneratedHeight: 5, - name: `someValidator${i + initialValidatorAmount}`, + name: `someValidator${i + existingSentStakesCount}`, reportMisbehaviorHeights: [], selfStake: BigInt(0), totalStake: BigInt(0), @@ -1241,54 +1285,61 @@ describe('StakeCommand', () => { await validatorStore.set( createStoreGetter(stateStore), - validatorAddress, + uniqueValidatorAddress, validatorInfo, ); stakes.push({ - validatorAddress, - amount: liskToBeddows(10), + validatorAddress: uniqueValidatorAddress, + amount, }); } - // Account already contains 8 positive stakes - // now we added 2 negative stakes and 3 new positive stakes - // which will make total positive stakes to grow over 10 + // Account can only take 2 more new upstakes + // now we remove 2 of existing stakes and add 7 new stakes + // which will make total stakes to grow over the allowed maximum transactionParamsDecoded = { stakes }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow('Sender can only stake upto 10.'); + await expect(stakeCommand.execute(context)).rejects.toThrow( + `Sender can only stake upto ${MAX_NUMBER_SENT_STAKES}.`, + ); + + // count events until the first failed one + const totalEventsCount = + MAX_NUMBER_SENT_STAKES - existingSentStakesCount + 2 * downstakeCount + 1; checkEventResult( context.eventQueue, - 5, + totalEventsCount, ValidatorStakedEvent, - 4, + totalEventsCount - 1, { senderAddress, - validatorAddress: stakes[4].validatorAddress, - amount: stakes[4].amount, + validatorAddress: stakes[totalEventsCount - 1].validatorAddress, + amount: stakes[totalEventsCount - 1].amount, }, PoSEventResult.STAKE_FAILED_TOO_MANY_SENT_STAKES, ); }); }); - describe('when transaction.params.stakes has negative amount and makes stakerData.pendingUnlocks more than 20 entries', () => { + describe(`when transaction.params.stakes has negative amount and makes stakerData.pendingUnlocks more than ${MAX_NUMBER_PENDING_UNLOCKS} entries`, () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_PENDING_UNLOCKS failure', async () => { - const initialValidatorAmountForUnlocks = 19; const stakerData = await stakerStore.getOrDefault( createStoreGetter(stateStore), senderAddress, ); - // Suppose account already 19 unlocking - for (let i = 0; i < initialValidatorAmountForUnlocks; i += 1) { - const validatorAddress = utils.getRandomBytes(20); + // Suppose account can have only 1 more pending unlock + for (let i = 0; i < MAX_NUMBER_PENDING_UNLOCKS - 1; i += 1) { + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19), Buffer.alloc(1, i)]); const validatorInfo = { consecutiveMissedBlocks: 0, @@ -1305,12 +1356,12 @@ describe('StakeCommand', () => { await validatorStore.set( createStoreGetter(stateStore), - validatorAddress, + uniqueValidatorAddress, validatorInfo, ); const pendingUnlock = { - validatorAddress, + validatorAddress: uniqueValidatorAddress, amount: liskToBeddows(20), unstakeHeight: i, }; @@ -1319,7 +1370,7 @@ describe('StakeCommand', () => { // Suppose account have 5 positive stakes for (let i = 0; i < 5; i += 1) { - const validatorAddress = utils.getRandomBytes(20); + const uniqueValidatorAddress = Buffer.concat([Buffer.alloc(19), Buffer.alloc(1, i)]); const validatorInfo = { consecutiveMissedBlocks: 0, @@ -1336,12 +1387,12 @@ describe('StakeCommand', () => { await validatorStore.set( createStoreGetter(stateStore), - validatorAddress, + uniqueValidatorAddress, validatorInfo, ); const stake = { - validatorAddress, + validatorAddress: uniqueValidatorAddress, amount: liskToBeddows(20), sharingCoefficients: [], }; @@ -1366,13 +1417,15 @@ describe('StakeCommand', () => { // now we added 2 negative stakes // which will make total unlocking to grow over 20 transactionParamsDecoded = { stakes }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow( + await expect(stakeCommand.execute(context)).rejects.toThrow( `Pending unlocks cannot exceed ${defaultConfig.maxNumberPendingUnlocks}.`, ); @@ -1414,13 +1467,15 @@ describe('StakeCommand', () => { }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await expect(command.execute(context)).rejects.toThrow( + await expect(stakeCommand.execute(context)).rejects.toThrow( 'The unstake amount exceeds the staked amount for this validator.', ); @@ -1443,12 +1498,10 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes contains self-stake', () => { const senderStakeAmountPositive = liskToBeddows(80); const senderStakeAmountNegative = liskToBeddows(20); - let totalStake: bigint; - let selfStake: bigint; - beforeEach(async () => { - totalStake = BigInt(20); - selfStake = BigInt(20); + const totalStake = liskToBeddows(20); + const selfStake = liskToBeddows(20); + beforeEach(async () => { const validatorInfo = { ...validator1, totalStake, @@ -1456,23 +1509,37 @@ describe('StakeCommand', () => { }; await validatorStore.set(createStoreGetter(stateStore), senderAddress, validatorInfo); + const stakerData = await stakerStore.getOrDefault( + createStoreGetter(stateStore), + senderAddress, + ); + const stake = { + validatorAddress: senderAddress, + amount: selfStake, + sharingCoefficients: [], + }; + stakerData.stakes.push(stake); + await stakerStore.set(createStoreGetter(stateStore), senderAddress, stakerData); + transactionParamsDecoded = { stakes: [{ validatorAddress: senderAddress, amount: senderStakeAmountPositive }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockClear(); }); - it('should update stakes and totalStake', async () => { - await command.execute(context); + it('should update stakes and totalStakeReceived', async () => { + await stakeCommand.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), @@ -1494,8 +1561,8 @@ describe('StakeCommand', () => { ); }); - it('should change validatorData.selfStake and totalStake with positive stake', async () => { - await command.execute(context); + it('should change validatorData.selfStake and totalStakeReceived with positive stake', async () => { + await stakeCommand.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), @@ -1506,24 +1573,26 @@ describe('StakeCommand', () => { expect(validatorData.selfStake).toEqual(selfStake + senderStakeAmountPositive); }); - it('should change validatorData.selfStake, totalStake and unlocking with negative stake', async () => { - await command.execute(context); + it('should change validatorData.selfStake, totalStakeReceived and pendingUnlocks after decreasing self-stake', async () => { + await stakeCommand.execute(context); transactionParamsDecoded = { stakes: [ { validatorAddress: senderAddress, amount: senderStakeAmountNegative * BigInt(-1) }, ], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), @@ -1540,11 +1609,10 @@ describe('StakeCommand', () => { expect(validatorData.selfStake).toEqual( totalStake + senderStakeAmountPositive - senderStakeAmountNegative, ); - expect(stakerData.stakes).toHaveLength(1); expect(stakerData.stakes).toEqual([ { validatorAddress: senderAddress, - amount: senderStakeAmountPositive - senderStakeAmountNegative, + amount: totalStake + senderStakeAmountPositive - senderStakeAmountNegative, sharingCoefficients: [ { tokenID: Buffer.alloc(8), @@ -1553,7 +1621,6 @@ describe('StakeCommand', () => { ], }, ]); - expect(stakerData.pendingUnlocks).toHaveLength(1); expect(stakerData.pendingUnlocks).toEqual([ { validatorAddress: senderAddress, @@ -1568,7 +1635,6 @@ describe('StakeCommand', () => { const senderStakeAmountPositive = liskToBeddows(80); const senderStakeAmountNegative = liskToBeddows(20); const validatorSelfStake = liskToBeddows(2000); - const validatorAddress = utils.getRandomBytes(20); let validatorInfo; beforeEach(async () => { validatorInfo = { @@ -1589,20 +1655,22 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress, amount: senderStakeAmountPositive }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); tokenLockMock.mockClear(); }); - it('should not change validatorData.selfStake but should update totalStake with positive stake', async () => { - await command.execute(context); + it('should not change validatorData.selfStake but should update totalStakeReceived with positive stake', async () => { + await stakeCommand.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), @@ -1613,22 +1681,24 @@ describe('StakeCommand', () => { expect(validatorData.selfStake).toEqual(validatorSelfStake); }); - it('should not change validatorData.selfStake but should change totalStake and unlocking with negative stake', async () => { - await command.execute(context); + it('should not change validatorData.selfStake but should change totalStakeReceived and unlocking with negative stake', async () => { + await stakeCommand.execute(context); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: senderStakeAmountNegative * BigInt(-1) }], }; - transaction.params = codec.encode(command.schema, transactionParamsDecoded); + + transaction.params = codec.encode(stakeCommand.schema, transactionParamsDecoded); + context = createTransactionContext({ transaction, stateStore, header: { height: lastBlockHeight, } as any, - }).createCommandExecuteContext(command.schema); + }).createCommandExecuteContext(stakeCommand.schema); - await command.execute(context); + await stakeCommand.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), @@ -1643,7 +1713,6 @@ describe('StakeCommand', () => { senderStakeAmountPositive - senderStakeAmountNegative + validatorSelfStake, ); expect(validatorData.selfStake).toEqual(validatorSelfStake); - expect(stakerData.stakes).toHaveLength(1); expect(stakerData.stakes).toEqual([ { validatorAddress, @@ -1651,7 +1720,6 @@ describe('StakeCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ]); - expect(stakerData.pendingUnlocks).toHaveLength(1); expect(stakerData.pendingUnlocks).toEqual([ { validatorAddress, diff --git a/framework/test/unit/modules/pos/commands/unlock.spec.ts b/framework/test/unit/modules/pos/commands/unlock.spec.ts index f3daae78bc2..6c592e6351f 100644 --- a/framework/test/unit/modules/pos/commands/unlock.spec.ts +++ b/framework/test/unit/modules/pos/commands/unlock.spec.ts @@ -42,17 +42,20 @@ describe('UnlockCommand', () => { let unlockCommand: Modules.PoS.UnlockCommand; let stateStore: PrefixedStateReadWriter; let validatorSubstore: ValidatorStore; - let stakerSubstore: StakerStore; + let stakerStore: StakerStore; let genesisSubstore: GenesisDataStore; let mockTokenMethod: TokenMethod; let blockHeight: number; let header: BlockHeader; let unlockableObject: UnlockingObject; let unlockableObject2: UnlockingObject; - let unlockableObject3: UnlockingObject; - let nonUnlockableObject: UnlockingObject; + let nonUnlockableObject1: UnlockingObject; + let nonUnlockableObject2: UnlockingObject; let context: StateMachine.CommandExecuteContext; - let storedData: StakerData; + let stakerData: StakerData; + + const initRounds = 3; + const validator1 = { name: 'validator1', address: utils.getRandomBytes(32), @@ -126,7 +129,7 @@ describe('UnlockCommand', () => { unlockCommand.init({ ...config, punishmentLockingPeriods }); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); validatorSubstore = pos.stores.get(ValidatorStore); - stakerSubstore = pos.stores.get(StakerStore); + stakerStore = pos.stores.get(StakerStore); genesisSubstore = pos.stores.get(GenesisDataStore); blockHeight = 8760000; header = testing.createFakeBlockHeader({ @@ -143,7 +146,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), validator1.address, { @@ -160,12 +163,12 @@ describe('UnlockCommand', () => { amount: validator1.amount, unstakeHeight: blockHeight - config.lockingPeriodStaking, }; - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: validator2.address, amount: validator2.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -173,7 +176,7 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [unlockableObject, nonUnlockableObject], + pendingUnlocks: [unlockableObject, nonUnlockableObject2], }); context = testing .createTransactionContext({ @@ -184,18 +187,15 @@ describe('UnlockCommand', () => { }) .createCommandExecuteContext(); await unlockCommand.execute(context); - storedData = await stakerSubstore.get( - createStoreGetter(stateStore), - transaction.senderAddress, - ); + stakerData = await stakerStore.get(createStoreGetter(stateStore), transaction.senderAddress); }); it('should remove eligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject); }); it('should not remove ineligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).toContainEqual(nonUnlockableObject); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject2); }); }); @@ -203,24 +203,29 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { ...defaultValidatorInfo, - name: 'nonpunishedselfstaker', + name: 'nonpunishedselfstaker1', }); + await validatorSubstore.set(createStoreGetter(stateStore), validator1.address, { + ...defaultValidatorInfo, + name: 'nonpunishedselfstaker2', + }); + unlockableObject = { validatorAddress: transaction.senderAddress, amount: validator1.amount, unstakeHeight: blockHeight - config.lockingPeriodSelfStaking, }; - nonUnlockableObject = { - validatorAddress: transaction.senderAddress, + nonUnlockableObject2 = { + validatorAddress: validator1.address, amount: validator2.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -228,12 +233,12 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, { - validatorAddress: nonUnlockableObject.validatorAddress, + validatorAddress: nonUnlockableObject2.validatorAddress, amount: validator2.amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [unlockableObject, nonUnlockableObject], + pendingUnlocks: [unlockableObject, nonUnlockableObject2], }); context = testing .createTransactionContext({ @@ -244,18 +249,15 @@ describe('UnlockCommand', () => { }) .createCommandExecuteContext(); await unlockCommand.execute(context); - storedData = await stakerSubstore.get( - createStoreGetter(stateStore), - transaction.senderAddress, - ); + stakerData = await stakerStore.get(createStoreGetter(stateStore), transaction.senderAddress); }); it('should remove eligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject); }); it('should not remove ineligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).toContainEqual(nonUnlockableObject); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject2); }); }); @@ -263,7 +265,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), validator1.address, { @@ -277,11 +279,11 @@ describe('UnlockCommand', () => { name: 'punishedstaker2', reportMisbehaviorHeights: [blockHeight], }); - // This covers scenario: has not waited pomHeight + 260,000 blocks but waited unstakeHeight + 2000 blocks and pomHeight is equal to unstakeHeight + 2000 blocks + // This covers scenario: has not waited pomHeight + 260,000 blocks but waited unstakeHeight + 2000 blocks and pomHeight is less than unstakeHeight + 2000 blocks await validatorSubstore.set(createStoreGetter(stateStore), validator3.address, { ...defaultValidatorInfo, name: 'punishedstaker3', - reportMisbehaviorHeights: [blockHeight - 1000], + reportMisbehaviorHeights: [blockHeight - 1001], }); await validatorSubstore.set(createStoreGetter(stateStore), validator4.address, { ...defaultValidatorInfo, @@ -298,17 +300,17 @@ describe('UnlockCommand', () => { amount: validator2.amount, unstakeHeight: blockHeight - config.lockingPeriodStaking - 1000, }; - unlockableObject3 = { + nonUnlockableObject1 = { validatorAddress: validator3.address, amount: validator3.amount, unstakeHeight: blockHeight - config.lockingPeriodStaking - 1000, }; - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: validator4.address, amount: validator4.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -321,21 +323,21 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, { - validatorAddress: unlockableObject3.validatorAddress, - amount: unlockableObject3.amount, + validatorAddress: nonUnlockableObject1.validatorAddress, + amount: nonUnlockableObject1.amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, { - validatorAddress: nonUnlockableObject.validatorAddress, - amount: nonUnlockableObject.amount, + validatorAddress: nonUnlockableObject2.validatorAddress, + amount: nonUnlockableObject2.amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], pendingUnlocks: [ unlockableObject, unlockableObject2, - unlockableObject3, - nonUnlockableObject, + nonUnlockableObject1, + nonUnlockableObject2, ], }); context = testing @@ -347,20 +349,17 @@ describe('UnlockCommand', () => { }) .createCommandExecuteContext(); await unlockCommand.execute(context); - storedData = await stakerSubstore.get( - createStoreGetter(stateStore), - transaction.senderAddress, - ); + stakerData = await stakerStore.get(createStoreGetter(stateStore), transaction.senderAddress); }); it('should remove eligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject); - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject2); - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject3); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject2); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject1); }); it('should not remove ineligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).toContainEqual(nonUnlockableObject); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject2); }); }); @@ -368,7 +367,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { @@ -381,12 +380,12 @@ describe('UnlockCommand', () => { amount: validator1.amount, unstakeHeight: blockHeight - config.lockingPeriodSelfStaking, }; - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: transaction.senderAddress, amount: validator2.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -394,12 +393,12 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, { - validatorAddress: nonUnlockableObject.validatorAddress, - amount: nonUnlockableObject.amount, + validatorAddress: nonUnlockableObject2.validatorAddress, + amount: nonUnlockableObject2.amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [unlockableObject, nonUnlockableObject], + pendingUnlocks: [unlockableObject, nonUnlockableObject2], }); context = testing .createTransactionContext({ @@ -410,18 +409,15 @@ describe('UnlockCommand', () => { }) .createCommandExecuteContext(); await unlockCommand.execute(context); - storedData = await stakerSubstore.get( - createStoreGetter(stateStore), - transaction.senderAddress, - ); + stakerData = await stakerStore.get(createStoreGetter(stateStore), transaction.senderAddress); }); it('should remove eligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject); }); it('should not remove ineligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).toContainEqual(nonUnlockableObject); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject2); }); }); @@ -429,7 +425,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { @@ -437,20 +433,20 @@ describe('UnlockCommand', () => { name: 'punishedselfstaker', reportMisbehaviorHeights: [blockHeight - 1], }); - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: transaction.senderAddress, amount: validator1.amount, unstakeHeight: blockHeight - config.lockingPeriodSelfStaking, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { - validatorAddress: nonUnlockableObject.validatorAddress, - amount: nonUnlockableObject.amount, + validatorAddress: nonUnlockableObject2.validatorAddress, + amount: nonUnlockableObject2.amount, sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [nonUnlockableObject], + pendingUnlocks: [nonUnlockableObject2], }); context = testing .createTransactionContext({ @@ -473,7 +469,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 10, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), validator1.address, { @@ -484,12 +480,12 @@ describe('UnlockCommand', () => { name: validator2.name, ...defaultValidatorInfo, }); - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: validator2.address, amount: validator2.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -497,7 +493,7 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [nonUnlockableObject], + pendingUnlocks: [nonUnlockableObject2], }); }); @@ -529,7 +525,7 @@ describe('UnlockCommand', () => { beforeEach(async () => { await genesisSubstore.set(createStoreGetter(stateStore), EMPTY_KEY, { height: 8760000, - initRounds: 1, + initRounds, initValidators: [], }); await validatorSubstore.set(createStoreGetter(stateStore), validator1.address, { @@ -546,12 +542,12 @@ describe('UnlockCommand', () => { amount: validator1.amount, unstakeHeight: blockHeight - config.lockingPeriodStaking, }; - nonUnlockableObject = { + nonUnlockableObject2 = { validatorAddress: validator2.address, amount: validator2.amount, unstakeHeight: blockHeight, }; - await stakerSubstore.set(createStoreGetter(stateStore), transaction.senderAddress, { + await stakerStore.set(createStoreGetter(stateStore), transaction.senderAddress, { stakes: [ { validatorAddress: unlockableObject.validatorAddress, @@ -559,7 +555,7 @@ describe('UnlockCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }, ], - pendingUnlocks: [unlockableObject, nonUnlockableObject], + pendingUnlocks: [unlockableObject, nonUnlockableObject2], }); context = testing .createTransactionContext({ @@ -570,18 +566,15 @@ describe('UnlockCommand', () => { }) .createCommandExecuteContext(); await unlockCommand.execute(context); - storedData = await stakerSubstore.get( - createStoreGetter(stateStore), - transaction.senderAddress, - ); + stakerData = await stakerStore.get(createStoreGetter(stateStore), transaction.senderAddress); }); it('should remove eligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).not.toContainEqual(unlockableObject); + expect(stakerData.pendingUnlocks).not.toContainEqual(unlockableObject); }); it('should not remove ineligible pending unlock from staker substore', () => { - expect(storedData.pendingUnlocks).toContainEqual(nonUnlockableObject); + expect(stakerData.pendingUnlocks).toContainEqual(nonUnlockableObject2); }); }); }); diff --git a/framework/test/unit/modules/pos/utils.spec.ts b/framework/test/unit/modules/pos/utils.spec.ts index a4913ebb8af..b159e2f3048 100644 --- a/framework/test/unit/modules/pos/utils.spec.ts +++ b/framework/test/unit/modules/pos/utils.spec.ts @@ -19,11 +19,37 @@ import { ModuleConfigJSON, StakeSharingCoefficient, } from '../../../../src/modules/pos/types'; -import { calculateStakeRewards, getModuleConfig } from '../../../../src/modules/pos/utils'; +import { + calculateStakeRewards, + getModuleConfig, + isUsername, +} from '../../../../src/modules/pos/utils'; const { q96 } = math; describe('utils', () => { + describe('isUsername', () => { + it('should return true for a valid username', () => { + expect(isUsername('valid_username$1')).toBeTrue(); + }); + + it('should return false for a username that contains empty character', () => { + expect(isUsername('invalid username')).toBeFalse(); + }); + + it('should return false for a username that contains non-supported special character', () => { + expect(isUsername('invalid#username')).toBeFalse(); + }); + + it('should return false for a username that contains UPPERCASE characters', () => { + expect(isUsername('InvalidUsername')).toBeFalse(); + }); + + it('should return false for a username that contains a null character', () => { + expect(isUsername('invalid_username\0')).toBeFalse(); + }); + }); + describe('getModuleConfig', () => { it('converts ModuleConfigJSON to ModuleConfig', () => { const expected: ModuleConfig = {