From d420033b3654302a22aa3a593baf53d1aca169a7 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 30 Mar 2022 08:07:07 +0100 Subject: [PATCH] Revert "Revert "Add Qortal AT FunctionCodes for getting account level / blocks minted + tests"" This reverts commit 59025b8f470374866af3477285c4c98838b06a45. --- src/main/java/org/qortal/account/Account.java | 6 + src/main/java/org/qortal/at/QortalATAPI.java | 2 +- .../org/qortal/at/QortalFunctionCode.java | 64 ++++++ .../qortal/repository/AccountRepository.java | 3 + .../hsqldb/HSQLDBAccountRepository.java | 14 ++ .../GetAccountBlocksMintedTests.java | 186 ++++++++++++++++++ .../GetAccountLevelTests.java | 184 +++++++++++++++++ 7 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountBlocksMintedTests.java create mode 100644 src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountLevelTests.java diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index aeff7810..df14d88f 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -205,6 +205,12 @@ public class Account { return false; } + /** Returns account's blockMinted (0+) or null if account not found in repository. */ + public Integer getBlocksMinted() throws DataException { + return this.repository.getAccountRepository().getMintedBlockCount(this.address); + } + + /** Returns whether account can build reward-shares. *

* To be able to create reward-shares, the account needs to pass at least one of these tests:
diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index c393a684..829c391f 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -551,7 +551,7 @@ public class QortalATAPI extends API { *

* Otherwise, assume B is a public key. */ - private Account getAccountFromB(MachineState state) { + /*package*/ Account getAccountFromB(MachineState state) { byte[] bBytes = this.getB(state); if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION) diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java index 7069290a..a2d43ac8 100644 --- a/src/main/java/org/qortal/at/QortalFunctionCode.java +++ b/src/main/java/org/qortal/at/QortalFunctionCode.java @@ -10,9 +10,11 @@ import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionData; import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.MachineState; +import org.qortal.account.Account; import org.qortal.crosschain.Bitcoin; import org.qortal.crypto.Crypto; import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; import org.qortal.settings.Settings; /** @@ -160,6 +162,68 @@ public enum QortalFunctionCode { protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { convertAddressInB(Crypto.ADDRESS_VERSION, state); } + }, + /** + * Returns account level of account in B.
+ * 0x0520
+ * B should contain either Qortal address or public key,
+ * e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}. + *

+ * Returns account level, or -1 if account unknown. + *

+ * @see QortalATAPI#getAccountFromB(MachineState) + */ + GET_ACCOUNT_LEVEL_FROM_ACCOUNT_IN_B(0x0520, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + QortalATAPI api = (QortalATAPI) state.getAPI(); + Account account = api.getAccountFromB(state); + + Integer accountLevel = null; + + if (account != null) { + try { + accountLevel = account.getLevel(); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch account level?", e); + } + } + + functionData.returnValue = accountLevel != null + ? accountLevel.longValue() + : -1; + } + }, + /** + * Returns account's minted block count of account in B.
+ * 0x0521
+ * B should contain either Qortal address or public key,
+ * e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}. + *

+ * Returns account level, or -1 if account unknown. + *

+ * @see QortalATAPI#getAccountFromB(MachineState) + */ + GET_BLOCKS_MINTED_FROM_ACCOUNT_IN_B(0x0521, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + QortalATAPI api = (QortalATAPI) state.getAPI(); + Account account = api.getAccountFromB(state); + + Integer blocksMinted = null; + + if (account != null) { + try { + blocksMinted = account.getBlocksMinted(); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch account's minted block count?", e); + } + } + + functionData.returnValue = blocksMinted != null + ? blocksMinted.longValue() + : -1; + } }; public final short value; diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index e4c53e9b..c1d31e31 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -76,6 +76,9 @@ public interface AccountRepository { */ public void setBlocksMintedAdjustment(AccountData accountData) throws DataException; + /** Returns account's minted block count or null if account not found. */ + public Integer getMintedBlockCount(String address) throws DataException; + /** * Saves account's minted block count and public key if present, in repository. *

diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index ca1b73cd..028f3d46 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -241,6 +241,20 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public Integer getMintedBlockCount(String address) throws DataException { + String sql = "SELECT blocks_minted FROM Accounts WHERE account = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { + if (resultSet == null) + return null; + + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to fetch account's minted block count from repository", e); + } + } + @Override public void setMintedBlockCount(AccountData accountData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); diff --git a/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountBlocksMintedTests.java b/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountBlocksMintedTests.java new file mode 100644 index 00000000..695ba291 --- /dev/null +++ b/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountBlocksMintedTests.java @@ -0,0 +1,186 @@ +package org.qortal.test.at.qortalfunctioncodes; + +import com.google.common.primitives.Bytes; +import org.ciyam.at.CompilationException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.at.QortalFunctionCode; +import org.qortal.data.at.ATStateData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.AtUtils; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TestAccount; +import org.qortal.transaction.DeployAtTransaction; +import org.qortal.utils.Base58; +import org.qortal.utils.BitTwiddling; + +import java.nio.ByteBuffer; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class GetAccountBlocksMintedTests extends Common { + + private static final Random RANDOM = new Random(); + private static final long fundingAmount = 1_00000000L; + + private Repository repository = null; + private byte[] creationBytes = null; + private PrivateKeyAccount deployer; + private DeployAtTransaction deployAtTransaction; + private String atAddress; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.repository = RepositoryManager.getRepository(); + this.deployer = Common.getTestAccount(repository, "alice"); + + } + + @After + public void after() throws DataException { + if (this.repository != null) + this.repository.close(); + + this.repository = null; + } + + @Test + public void testGetAccountBlocksMintedFromAddress() throws DataException { + Account alice = Common.getTestAccount(repository, "alice"); + byte[] accountBytes = Bytes.ensureCapacity(Base58.decode(alice.getAddress()), 32, 0); + + this.creationBytes = buildGetAccountBlocksMintedAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run - Alice's blocksMinted is incremented AFTER block is processed / AT is run + Integer expectedBlocksMinted = alice.getBlocksMinted(); + BlockUtils.mintBlock(repository); + + Integer extractedBlocksMinted = extractBlocksMinted(repository, atAddress); + assertEquals(expectedBlocksMinted, extractedBlocksMinted); + } + + @Test + public void testGetAccountBlocksMintedFromPublicKey() throws DataException { + TestAccount alice = Common.getTestAccount(repository, "alice"); + byte[] accountBytes = alice.getPublicKey(); + + this.creationBytes = buildGetAccountBlocksMintedAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run - Alice's blocksMinted is incremented AFTER block is processed / AT is run + Integer expectedBlocksMinted = alice.getBlocksMinted(); + BlockUtils.mintBlock(repository); + + Integer extractedBlocksMinted = extractBlocksMinted(repository, atAddress); + assertEquals(expectedBlocksMinted, extractedBlocksMinted); + } + + @Test + public void testGetUnknownAccountBlocksMinted() throws DataException { + byte[] accountBytes = new byte[32]; + RANDOM.nextBytes(accountBytes); + + this.creationBytes = buildGetAccountBlocksMintedAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run - Alice's blocksMinted is incremented AFTER block is processed / AT is run + BlockUtils.mintBlock(repository); + + Integer extractedBlocksMinted = extractBlocksMinted(repository, atAddress); + assertNull(extractedBlocksMinted); + } + + private static byte[] buildGetAccountBlocksMintedAT(byte[] accountBytes) { + // Labels for data segment addresses + int addrCounter = 0; + + // Beginning of data segment for easy extraction + final int addrBlocksMinted = addrCounter++; + + // accountBytes + final int addrAccountBytes = addrCounter; + addrCounter += 4; + + // Pointer to accountBytes so we can load them into B + final int addrAccountBytesPointer = addrCounter++; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + // Write accountBytes + dataByteBuffer.position(addrAccountBytes * MachineState.VALUE_SIZE); + dataByteBuffer.put(accountBytes); + + // Store pointer to addrAccountbytes at addrAccountBytesPointer + assertEquals(addrAccountBytesPointer * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.putLong(addrAccountBytes); + + ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + try { + /* Initialization */ + + // Copy accountBytes from data segment into B, starting at addrAccountBytes (as pointed to by addrAccountBytesPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAccountBytesPointer)); + + // Get account's blocks minted count and save into addrBlocksMinted + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_BLOCKS_MINTED_FROM_ACCOUNT_IN_B.value, addrBlocksMinted)); + + // We're done + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + } catch (CompilationException e) { + throw new IllegalStateException("Unable to compile AT?", e); + } + } + + codeByteBuffer.flip(); + + byte[] codeBytes = new byte[codeByteBuffer.limit()]; + codeByteBuffer.get(codeBytes); + + final short ciyamAtVersion = 2; + final short numCallStackPages = 0; + final short numUserStackPages = 0; + final long minActivationAmount = 0L; + + return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); + } + + private Integer extractBlocksMinted(Repository repository, String atAddress) throws DataException { + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + byte[] dataBytes = MachineState.extractDataBytes(stateData); + + Long blocksMintedValue = BitTwiddling.longFromBEBytes(dataBytes, 0); + if (blocksMintedValue == -1) + return null; + + return blocksMintedValue.intValue(); + } +} diff --git a/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountLevelTests.java b/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountLevelTests.java new file mode 100644 index 00000000..dfb6d017 --- /dev/null +++ b/src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountLevelTests.java @@ -0,0 +1,184 @@ +package org.qortal.test.at.qortalfunctioncodes; + +import com.google.common.primitives.Bytes; +import org.ciyam.at.CompilationException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.at.QortalFunctionCode; +import org.qortal.data.at.ATStateData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.AtUtils; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TestAccount; +import org.qortal.transaction.DeployAtTransaction; +import org.qortal.utils.Base58; +import org.qortal.utils.BitTwiddling; + +import java.nio.ByteBuffer; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class GetAccountLevelTests extends Common { + + private static final Random RANDOM = new Random(); + private static final long fundingAmount = 1_00000000L; + + private Repository repository = null; + private byte[] creationBytes = null; + private PrivateKeyAccount deployer; + private DeployAtTransaction deployAtTransaction; + private String atAddress; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.repository = RepositoryManager.getRepository(); + this.deployer = Common.getTestAccount(repository, "alice"); + + } + + @After + public void after() throws DataException { + if (this.repository != null) + this.repository.close(); + + this.repository = null; + } + + @Test + public void testGetAccountLevelFromAddress() throws DataException { + Account dilbert = Common.getTestAccount(repository, "dilbert"); + byte[] accountBytes = Bytes.ensureCapacity(Base58.decode(dilbert.getAddress()), 32, 0); + + this.creationBytes = buildGetAccountLevelAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run + BlockUtils.mintBlock(repository); + + Integer extractedAccountLevel = extractAccountLevel(repository, atAddress); + assertEquals(dilbert.getLevel(), extractedAccountLevel); + } + + @Test + public void testGetAccountLevelFromPublicKey() throws DataException { + TestAccount dilbert = Common.getTestAccount(repository, "dilbert"); + byte[] accountBytes = dilbert.getPublicKey(); + + this.creationBytes = buildGetAccountLevelAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run + BlockUtils.mintBlock(repository); + + Integer extractedAccountLevel = extractAccountLevel(repository, atAddress); + assertEquals(dilbert.getLevel(), extractedAccountLevel); + } + + @Test + public void testGetUnknownAccountLevel() throws DataException { + byte[] accountBytes = new byte[32]; + RANDOM.nextBytes(accountBytes); + + this.creationBytes = buildGetAccountLevelAT(accountBytes); + + this.deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a block to allow AT to run + BlockUtils.mintBlock(repository); + + Integer extractedAccountLevel = extractAccountLevel(repository, atAddress); + assertNull(extractedAccountLevel); + } + + private static byte[] buildGetAccountLevelAT(byte[] accountBytes) { + // Labels for data segment addresses + int addrCounter = 0; + + // Beginning of data segment for easy extraction + final int addrAccountLevel = addrCounter++; + + // accountBytes + final int addrAccountBytes = addrCounter; + addrCounter += 4; + + // Pointer to accountBytes so we can load them into B + final int addrAccountBytesPointer = addrCounter++; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + // Write accountBytes + dataByteBuffer.position(addrAccountBytes * MachineState.VALUE_SIZE); + dataByteBuffer.put(accountBytes); + + // Store pointer to addrAccountbytes at addrAccountBytesPointer + assertEquals(addrAccountBytesPointer * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.putLong(addrAccountBytes); + + ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + try { + /* Initialization */ + + // Copy accountBytes from data segment into B, starting at addrAccountBytes (as pointed to by addrAccountBytesPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAccountBytesPointer)); + + // Get account level and save into addrAccountLevel + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_ACCOUNT_LEVEL_FROM_ACCOUNT_IN_B.value, addrAccountLevel)); + + // We're done + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + } catch (CompilationException e) { + throw new IllegalStateException("Unable to compile AT?", e); + } + } + + codeByteBuffer.flip(); + + byte[] codeBytes = new byte[codeByteBuffer.limit()]; + codeByteBuffer.get(codeBytes); + + final short ciyamAtVersion = 2; + final short numCallStackPages = 0; + final short numUserStackPages = 0; + final long minActivationAmount = 0L; + + return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); + } + + private Integer extractAccountLevel(Repository repository, String atAddress) throws DataException { + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + byte[] dataBytes = MachineState.extractDataBytes(stateData); + + Long accountLevelValue = BitTwiddling.longFromBEBytes(dataBytes, 0); + if (accountLevelValue == -1) + return null; + + return accountLevelValue.intValue(); + } +}