Browse Source

Merge pull request #63 from catbref/master

Add Qortal AT FunctionCodes for getting account level / blocks minted
name-fixes
CalDescent 3 years ago committed by GitHub
parent
commit
f3e1fc884c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/main/java/org/qortal/account/Account.java
  2. 2
      src/main/java/org/qortal/at/QortalATAPI.java
  3. 64
      src/main/java/org/qortal/at/QortalFunctionCode.java
  4. 3
      src/main/java/org/qortal/repository/AccountRepository.java
  5. 14
      src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
  6. 186
      src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountBlocksMintedTests.java
  7. 184
      src/test/java/org/qortal/test/at/qortalfunctioncodes/GetAccountLevelTests.java

6
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.
* <p>
* To be able to create reward-shares, the account needs to pass at least one of these tests:<br>

2
src/main/java/org/qortal/at/QortalATAPI.java

@ -551,7 +551,7 @@ public class QortalATAPI extends API {
* <p>
* 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)

64
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.<br>
* <tt>0x0520</tt><br>
* B should contain either Qortal address or public key,<br>
* e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}</code>.
* <p></p>
* Returns account level, or -1 if account unknown.
* <p></p>
* @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.<br>
* <tt>0x0521</tt><br>
* B should contain either Qortal address or public key,<br>
* e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}</code>.
* <p></p>
* Returns account level, or -1 if account unknown.
* <p></p>
* @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;

3
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.
* <p>

14
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");

186
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();
}
}

184
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();
}
}
Loading…
Cancel
Save