Fix for chain-stall relating to freshly cancelled reward-shares.

In some cases, a freshly cancelled reward-share could still have
an associated signed timestamp. Block.mint() failed to spot this
and used an incorrect "online account" index when building the
to-be-minted block.

Block.mint() now checks that AccountRepository.getRewardShareIndex()
doesn't return null, i.e. indicating that the associated reward-share
for that "online account" no longer exists.

In turn, AccountRepository.getRewardShareIndex() didn't fulfill its
contract of returning null when the passed public key wasn't present
in the repository. So this method has been corrected also.

AccountRepository.rewardShareExists(byte[] publicKey) : boolean added.

BlockMinter had another bug where it didn't check the return from
Block.remint() for null properly. This has been fixed.

BlockMinter now has additional logging, with cool-off to prevent log
spam, for situations where minting could not happen.

Unit test (DisagreementTests) added to cover cancelled reward-share
case above. BlockMinter testing support slightly modified to help.
This commit is contained in:
catbref
2020-05-05 11:09:46 +01:00
parent e5cf76f3e0
commit 0cc9cd728e
5 changed files with 187 additions and 14 deletions

View File

@@ -0,0 +1,125 @@
package org.qortal.test.minting;
import static org.junit.Assert.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.BlockMinter;
import org.qortal.controller.Controller;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AccountUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transform.block.BlockTransformer;
import org.roaringbitmap.IntIterator;
import io.druid.extendedset.intset.ConciseSet;
public class DisagreementTests extends Common {
private static final BigDecimal CANCEL_SHARE_PERCENT = BigDecimal.ONE.negate();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
/**
* Testing minting a block when there is a signed online account timestamp present
* that no longer has a corresponding reward-share in DB.
* <p>
* Something like:
* <ul>
* <li>Mint block, with tx to create reward-share R</li>
* <li>Sign current timestamp with R</li>
* <li>Mint block including R as online account</li>
* <li>Mint block, with tx to cancel reward-share R</li>
* <li>Mint another block: R's timestamp should be excluded</li>
* </ul>
*
* @throws DataException
*/
@Test
public void testOnlineAccounts() throws DataException {
final BigDecimal sharePercent = new BigDecimal("12.8");
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
TestAccount signingAccount = Common.getTestAccount(repository, "alice");
// Create reward-share
byte[] testRewardSharePrivateKey = AccountUtils.rewardShare(repository, "alice", "bob", sharePercent);
PrivateKeyAccount testRewardShareAccount = new PrivateKeyAccount(repository, testRewardSharePrivateKey);
// Confirm reward-share info set correctly
RewardShareData testRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey());
assertNotNull(testRewardShareData);
// Create signed timestamps
Controller.getInstance().ensureTestingAccountsOnline(mintingAccount, testRewardShareAccount);
// Mint another block
BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount);
// Confirm reward-share's signed timestamp is included
BlockData blockData = repository.getBlockRepository().getLastBlock();
List<RewardShareData> rewardSharesData = fetchRewardSharesForBlock(repository, blockData);
boolean doesContainRewardShare = rewardSharesData.stream().anyMatch(rewardShareData -> Arrays.equals(rewardShareData.getRewardSharePublicKey(), testRewardShareData.getRewardSharePublicKey()));
assertTrue(doesContainRewardShare);
// Cancel reward-share
TransactionData cancelRewardShareTransactionData = AccountUtils.createRewardShare(repository, "alice", "bob", CANCEL_SHARE_PERCENT);
TransactionUtils.signAsUnconfirmed(repository, cancelRewardShareTransactionData, signingAccount);
BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount);
// Confirm reward-share no longer exists in repository
RewardShareData cancelledRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey());
assertNull("Reward-share shouldn't exist", cancelledRewardShareData);
// Attempt to mint with cancelled reward-share
BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount);
// Confirm reward-share's signed timestamp is NOT included
blockData = repository.getBlockRepository().getLastBlock();
rewardSharesData = fetchRewardSharesForBlock(repository, blockData);
doesContainRewardShare = rewardSharesData.stream().anyMatch(rewardShareData -> Arrays.equals(rewardShareData.getRewardSharePublicKey(), testRewardShareData.getRewardSharePublicKey()));
assertFalse(doesContainRewardShare);
}
}
private List<RewardShareData> fetchRewardSharesForBlock(Repository repository, BlockData blockData) throws DataException {
byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts();
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(encodedOnlineAccounts);
List<RewardShareData> rewardSharesData = new ArrayList<>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
rewardSharesData.add(rewardShareData);
}
return rewardSharesData;
}
}