CIYAM-ATv2 can be used with any asset, not just QORA.

Some initial BTC cross-chain support. (Needs more work).

Unified timestamp for V2 switchover to block version 4,
applicable to several transaction types.

Qora-specific interface to CIYAM ATv2 library.

Beware: some areas still work-in-progress!
This commit is contained in:
catbref 2018-12-03 13:05:38 +00:00
parent eaad565765
commit 90094be95a
33 changed files with 1177 additions and 239 deletions

View File

@ -0,0 +1,3 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations

View File

@ -1,5 +1,4 @@
import api.ApiClient;
import api.ApiService;
import repository.DataException;
import repository.RepositoryFactory;
@ -22,4 +21,5 @@ public class Start {
//String test = client.executeCommand("GET blocks/first");
//System.out.println(test);
}
}

View File

@ -3,12 +3,9 @@ import java.security.SecureRandom;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qora.account.PrivateKeyAccount;
import qora.block.BlockChain;
import qora.block.BlockGenerator;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import utils.Base58;
public class blockgenerator {

304
src/crosschain/BTC.java Normal file
View File

@ -0,0 +1,304 @@
package crosschain;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.SPVBlockStore;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.KeyChainGroup;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import settings.Settings;
public class BTC {
private static class RollbackBlockChain extends BlockChain {
public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
super(params, blockStore);
}
@Override
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
super.setChainHead(chainHead);
}
}
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
private static final int checkpointInterval = 500;
private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
super(params, getMinimalTextFileStream(params));
}
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
super(params, inputStream);
}
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
if (params == MainNetParams.get())
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
if (params == TestNet3Params.get())
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
}
@Override
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
int height = block.getHeight();
if (height % checkpointInterval == 0)
checkpoints.put(block.getHeader().getTimeSeconds(), block);
}
public void saveAsText(File textFile) throws FileNotFoundException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
writer.println("TXT CHECKPOINTS 1");
writer.println("0"); // Number of signatures to read. Do this later.
writer.println(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
}
}
}
public void saveAsBinary(File file) throws IOException {
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
MessageDigest digest = Sha256Hash.newDigest();
try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) {
digestOutputStream.on(false);
try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) {
dataOutputStream.writeBytes("CHECKPOINTS 1");
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
digestOutputStream.on(true);
dataOutputStream.writeInt(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
dataOutputStream.write(buffer.array());
buffer.position(0);
}
}
}
}
}
}
private static BTC instance;
private static final Object instanceLock = new Object();
private static File directory;
private static String chainFileName;
private static String checkpointsFileName;
private static NetworkParameters params;
private static PeerGroup peerGroup;
private static BlockStore blockStore;
private static RollbackBlockChain chain;
private static UpdateableCheckpointManager manager;
private BTC() {
// Start wallet
if (Settings.getInstance().isTestNet()) {
params = TestNet3Params.get();
chainFileName = "bitcoinj-testnet.spvchain";
checkpointsFileName = "checkpoints-testnet.txt";
} else {
params = MainNetParams.get();
chainFileName = "bitcoinj.spvchain";
checkpointsFileName = "checkpoints.txt";
}
directory = new File("Qora-BTC");
if (!directory.exists())
directory.mkdirs();
File chainFile = new File(directory, chainFileName);
try {
blockStore = new SPVBlockStore(params, chainFile);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
}
File checkpointsFile = new File(directory, checkpointsFileName);
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
manager = new UpdateableCheckpointManager(params, checkpointsStream);
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
manager = new UpdateableCheckpointManager(params);
} catch (IOException e2) {
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load BTC checkpoints", e);
}
try {
chain = new RollbackBlockChain(params, blockStore);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to construct BTC blockchain", e);
}
peerGroup = new PeerGroup(params, chain);
peerGroup.setUserAgent("qqq", "1.0");
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
peerGroup.start();
}
public static BTC getInstance() {
if (instance == null)
synchronized (instanceLock) {
if (instance == null)
instance = new BTC();
}
return instance;
}
public void shutdown() {
synchronized (instanceLock) {
if (instance == null)
return;
instance = null;
}
peerGroup.stop();
try {
blockStore.close();
} catch (BlockStoreException e) {
// What can we do?
}
}
protected Wallet createEmptyWallet() {
ECKey dummyKey = new ECKey();
KeyChainGroup keyChainGroup = new KeyChainGroup(params);
keyChainGroup.importKeys(dummyKey);
Wallet wallet = new Wallet(params, keyChainGroup);
wallet.removeKey(dummyKey);
return wallet;
}
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
Wallet wallet = createEmptyWallet();
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
System.out.println("Coins received via transaction " + tx.getHashAsString());
}
};
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
Address address = Address.fromBase58(params, base58Address);
wallet.addWatchedAddress(address, startTime);
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
chain.addWallet(wallet);
peerGroup.addWallet(wallet);
peerGroup.setFastCatchupTimeSecs(startTime);
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
peerGroup.removeWallet(wallet);
chain.removeWallet(wallet);
for (TransactionOutput output : outputs)
System.out.println(output.toString());
}
public void watch(Script script) {
// wallet.addWatchedScripts(scripts);
}
public void updateCheckpoints() {
final long now = new Date().getTime() / 1000;
try {
StoredBlock checkpoint = manager.getCheckpointBefore(now);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to update BTC checkpoints", e);
}
peerGroup.setFastCatchupTimeSecs(now);
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
peerGroup.downloadBlockChain();
try {
manager.saveAsText(new File(directory, checkpointsFileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
}
}
}

View File

@ -9,6 +9,7 @@ public class ATData {
private byte[] creatorPublicKey;
private long creation;
private int version;
private long assetId;
private byte[] codeBytes;
private boolean isSleeping;
private Integer sleepUntilHeight;
@ -19,12 +20,13 @@ public class ATData {
// Constructors
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight,
boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
this.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey;
this.creation = creation;
this.version = version;
this.assetId = assetId;
this.codeBytes = codeBytes;
this.isSleeping = isSleeping;
this.sleepUntilHeight = sleepUntilHeight;
@ -34,13 +36,14 @@ public class ATData {
this.frozenBalance = frozenBalance;
}
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight,
boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
this(ATAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null);
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
(BigDecimal) null);
// Convert Long frozenBalance to BigDecimal
if (frozenBalance != null)
this.frozenBalance = BigDecimal.valueOf(frozenBalance).setScale(8).divide(BigDecimal.valueOf(1e8));
this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8);
}
// Getters / setters
@ -61,6 +64,10 @@ public class ATData {
return this.version;
}
public long getAssetId() {
return this.assetId;
}
public byte[] getCodeBytes() {
return this.codeBytes;
}

View File

@ -7,6 +7,8 @@ import java.io.Serializable;
public class BlockData implements Serializable {
private static final long serialVersionUID = -7678329659124664620L;
private byte[] signature;
private int version;
private byte[] reference;
@ -21,8 +23,11 @@ public class BlockData implements Serializable {
private int atCount;
private BigDecimal atFees;
private BlockData() {} // necessary for JAX-RS serialization
// necessary for JAX-RS serialization
@SuppressWarnings("unused")
private BlockData() {
}
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
this.version = version;

View File

@ -13,12 +13,13 @@ public class DeployATTransactionData extends TransactionData {
private String tags;
private byte[] creationBytes;
private BigDecimal amount;
private long assetId;
private String ATAddress;
// Constructors
public DeployATTransactionData(String ATAddress, byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.DEPLOY_AT, fee, creatorPublicKey, timestamp, reference, signature);
this.name = name;
@ -26,18 +27,19 @@ public class DeployATTransactionData extends TransactionData {
this.ATType = ATType;
this.tags = tags;
this.amount = amount;
this.assetId = assetId;
this.creationBytes = creationBytes;
this.ATAddress = ATAddress;
}
public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, signature);
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, signature);
}
public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, null);
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, null);
}
// Getters/Setters
@ -66,6 +68,10 @@ public class DeployATTransactionData extends TransactionData {
return this.amount;
}
public long getAssetId() {
return this.assetId;
}
public String getATAddress() {
return this.ATAddress;
}

View File

@ -4,7 +4,6 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import data.account.AccountData;
import qora.transaction.Transaction.TransactionType;
public abstract class TransactionData {

View File

@ -9,6 +9,7 @@ import org.ciyam.at.MachineState;
import data.at.ATData;
import data.at.ATStateData;
import data.transaction.DeployATTransactionData;
import qora.assets.Asset;
import qora.crypto.Crypto;
import qora.transaction.ATTransaction;
import repository.ATRepository;
@ -44,14 +45,15 @@ public class AT {
long creation = deployATTransactionData.getTimestamp();
byte[] creationBytes = deployATTransactionData.getCreationBytes();
long assetId = deployATTransactionData.getAssetId();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(),
machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(),
machineState.getFrozenBalance());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
machineState.getIsFrozen(), machineState.getFrozenBalance());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
@ -95,8 +97,8 @@ public class AT {
boolean isFrozen = false;
Long frozenBalance = null;
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
frozenBalance);
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORA, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}

View File

@ -0,0 +1,134 @@
package qora.at;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import data.block.BlockData;
import data.transaction.ATTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.block.Block;
import qora.transaction.Transaction;
import repository.BlockRepository;
import repository.DataException;
public enum BlockchainAPI {
QORA(0) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
QoraATAPI api = (QoraATAPI) state.getAPI();
Account recipientAccount = new Account(api.repository, recipient);
BlockRepository blockRepository = api.repository.getBlockRepository();
try {
while (height <= blockRepository.getBlockchainHeight()) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(api.repository, blockData);
List<Transaction> transactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= transactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = transactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
// Found a transaction
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] hash = QoraATAPI.sha192(transaction.getTransactionData().getSignature());
api.setA2(state, QoraATAPI.fromBytes(hash, 0));
api.setA3(state, QoraATAPI.fromBytes(hash, 8));
api.setA4(state, QoraATAPI.fromBytes(hash, 16));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
api.zeroA(state);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
QoraATAPI api = (QoraATAPI) state.getAPI();
TransactionData transactionData = api.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
case AT:
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount.unscaledValue().longValue();
else
return 0xffffffffffffffffL;
default:
return 0xffffffffffffffffL;
}
}
},
BTC(1) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
// TODO
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
// TODO
return 0;
}
};
public final int value;
private final static Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
BlockchainAPI(int value) {
this.value = value;
}
public static BlockchainAPI valueOf(int value) {
return map.get(value);
}
// Blockchain-specific API methods
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
}

View File

@ -1,8 +1,6 @@
package qora.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@ -22,16 +20,12 @@ import data.at.ATData;
import data.block.BlockData;
import data.transaction.ATTransactionData;
import data.transaction.MessageTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.block.Block;
import qora.crypto.Crypto;
import qora.transaction.ATTransaction;
import qora.transaction.Transaction;
import repository.BlockRepository;
import repository.DataException;
import repository.Repository;
@ -116,10 +110,7 @@ public class QoraATAPI extends API {
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
byte[] blockHash = Crypto.digest(blockData.getSignature());
this.setA1(state, fromBytes(blockHash, 0));
this.setA2(state, fromBytes(blockHash, 8));
this.setA3(state, fromBytes(blockHash, 16));
this.setA4(state, fromBytes(blockHash, 24));
this.setA(state, blockHash);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
@ -127,57 +118,11 @@ public class QoraATAPI extends API {
@Override
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
// "Timestamp" is block height and transaction sequence
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
// Recipient is this AT
String recipient = this.atData.getATAddress();
Account atAccount = new Account(this.repository, this.atData.getATAddress());
BlockRepository blockRepository = this.repository.getBlockRepository();
try {
while (height <= blockRepository.getBlockchainHeight()) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(this.repository, blockData);
List<Transaction> transactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= transactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = transactions.get(sequence);
// Transaction needs to be sent to this AT
if (transaction.getRecipientAccounts().contains(atAccount)) {
// Found a transaction
this.setA1(state, new Timestamp(height, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] hash = sha192(transaction.getTransactionData().getSignature());
this.setA2(state, fromBytes(hash, 0));
this.setA3(state, fromBytes(hash, 8));
this.setA4(state, fromBytes(hash, 16));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
this.zeroA(state);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
}
@Override
@ -204,23 +149,9 @@ public class QoraATAPI extends API {
@Override
public long getAmountFromTransactionInA(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
case AT:
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount.unscaledValue().longValue();
else
return 0xffffffffffffffffL;
default:
return 0xffffffffffffffffL;
}
Timestamp timestamp = new Timestamp(state.getA1());
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
}
@Override
@ -295,13 +226,7 @@ public class QoraATAPI extends API {
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
ByteBuffer digestByteBuffer = ByteBuffer.wrap(paddedMessageData);
digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.setB1(state, digestByteBuffer.getLong());
this.setB2(state, digestByteBuffer.getLong());
this.setB3(state, digestByteBuffer.getLong());
this.setB4(state, digestByteBuffer.getLong());
this.setB(state, paddedMessageData);
}
@Override
@ -311,14 +236,7 @@ public class QoraATAPI extends API {
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = transactionData.getCreatorPublicKey();
// Enforce endian
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.setB1(state, byteBuffer.getLong());
this.setB2(state, byteBuffer.getLong());
this.setB3(state, byteBuffer.getLong());
this.setB4(state, byteBuffer.getLong());
this.setB(state, bytes);
}
@Override
@ -326,19 +244,12 @@ public class QoraATAPI extends API {
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = atData.getCreatorPublicKey();
// Enforce endian
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.setB1(state, byteBuffer.getLong());
this.setB2(state, byteBuffer.getLong());
this.setB3(state, byteBuffer.getLong());
this.setB4(state, byteBuffer.getLong());
this.setB(state, bytes);
}
@Override
public long getCurrentBalance(MachineState state) {
Account atAccount = new Account(this.repository, this.atData.getATAddress());
Account atAccount = this.getATAccount();
try {
return atAccount.getConfirmedBalance(Asset.QORA).unscaledValue().longValue();
@ -348,15 +259,39 @@ public class QoraATAPI extends API {
}
@Override
public void payAmountToB(long amount, MachineState state) {
// TODO Auto-generated method stub
public void payAmountToB(long unscaledAmount, MachineState state) {
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), amount, this.atData.getAssetId(),
new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
this.transactions.add(atTransaction);
}
@Override
public void messageAToB(MachineState state) {
// TODO Auto-generated method stub
byte[] message = state.getA();
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), BigDecimal.ZERO,
this.atData.getAssetId(), message, BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
this.transactions.add(atTransaction);
}
@Override
@ -377,8 +312,8 @@ public class QoraATAPI extends API {
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, Asset.QORA, null,
BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, this.atData.getAssetId(),
new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
@ -391,27 +326,33 @@ public class QoraATAPI extends API {
}
@Override
public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException {
// Currently not in use
throw new IllegalFunctionCodeException("AT API platform-specific functions currently not in use");
public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode)
throws IllegalFunctionCodeException {
QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode);
if (qoraFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qora function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qoraFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
}
@Override
public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException {
// Currently not in use
throw new ExecutionException("AT API platform-specific functions currently not in use");
public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode);
qoraFunctionCode.execute(functionData, state, rawFunctionCode);
}
// Utility methods
/** Convert part of little-endian byte[] to long */
private static long fromBytes(byte[] bytes, int start) {
/* package */ static long fromBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
}
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
private static byte[] sha192(byte[] input) {
public static byte[] sha192(byte[] input) {
try {
// SHA2-192
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
@ -431,7 +372,7 @@ public class QoraATAPI extends API {
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
private TransactionData fetchTransaction(MachineState state) {
/* package */ TransactionData fetchTransaction(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
try {
@ -450,6 +391,11 @@ public class QoraATAPI extends API {
}
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
}
/** Returns AT's creator's account */
private PublicKeyAccount getCreator() {
return new PublicKeyAccount(this.repository, this.atData.getCreatorPublicKey());
@ -478,7 +424,7 @@ public class QoraATAPI extends API {
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
// No transactions yet, so look up AT's account's last reference from repository
Account atAccount = new Account(this.repository, this.atData.getATAddress());
Account atAccount = this.getATAccount();
try {
return atAccount.getLastReference();

View File

@ -0,0 +1,107 @@
package qora.at;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
/**
* Qora-specific CIYAM-AT Functions.
* <p>
* Function codes need to be between 0x0500 and 0x06ff.
*
*/
public enum QoraFunctionCode {
/**
* <tt>0x0500</tt><br>
* Returns current BTC block's "timestamp"
*/
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
}
},
/**
* <tt>0x0501</tt><br>
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
*/
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
Timestamp timestamp = new Timestamp(functionData.value2);
try {
String recipient = new String(state.getB(), "UTF-8");
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
} catch (UnsupportedEncodingException e) {
throw new ExecutionException("Couldn't parse recipient from B", e);
}
}
};
public final short value;
public final int paramCount;
public final boolean returnsValue;
private final static Map<Short, QoraFunctionCode> map = Arrays.stream(QoraFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
private QoraFunctionCode(int value, int paramCount, boolean returnsValue) {
this.value = (short) value;
this.paramCount = paramCount;
this.returnsValue = returnsValue;
}
public static QoraFunctionCode valueOf(int value) {
return map.get((short) value);
}
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
if (paramCount != this.paramCount)
throw new IllegalFunctionCodeException(
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
if (returnValueExpected != this.returnsValue)
throw new IllegalFunctionCodeException(
"Passed returnValueExpected (" + returnValueExpected + ") does not match function's return signature (" + this.returnsValue + ")");
}
/**
* Execute Function
* <p>
* Can modify various fields of <tt>state</tt>, including <tt>programCounter</tt>.
* <p>
* Throws a subclass of <tt>ExecutionException</tt> on error, e.g. <tt>InvalidAddressException</tt>.
*
* @param functionData
* @param state
* @throws ExecutionException
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
if (functionData.paramCount >= 1 && functionData.value1 == null)
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
if (functionData.paramCount == 2 && functionData.value2 == null)
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
state.getLogger().debug("Function \"" + this.name() + "\"");
postCheckExecute(functionData, state, rawFunctionCode);
}
/** Actually execute function */
abstract protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
}

View File

@ -258,7 +258,7 @@ public class Block {
return 1;
else if (this.blockData.getTimestamp() < BlockChain.getPowFixReleaseTimestamp())
return 2;
else if (this.blockData.getTimestamp() < BlockChain.getDeployATV2Timestamp())
else if (this.blockData.getTimestamp() < BlockChain.getQoraV2Timestamp())
return 3;
else
return 4;

View File

@ -39,11 +39,7 @@ public class BlockChain {
private static final long VOTING_RELEASE_TIMESTAMP = 1403715600000L; // 2014-06-25T17:00:00+00:00
private static final long ARBITRARY_RELEASE_TIMESTAMP = 1405702800000L; // 2014-07-18T17:00:00+00:00
private static final long CREATE_POLL_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE POLL transactions
private static final long ISSUE_ASSET_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ISSUE ASSET transactions
private static final long CREATE_ORDER_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE ORDER transactions
private static final long ARBITRARY_TRANSACTION_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ARBITRARY transactions
private static final long DEPLOY_AT_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 DEPLOY AT transactions
private static final long QORA_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 blocks and transactions
/**
* Some sort start-up/initialization/checking method.
@ -146,39 +142,11 @@ public class BlockChain {
return ARBITRARY_RELEASE_TIMESTAMP;
}
public static long getCreatePollV2Timestamp() {
public static long getQoraV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return CREATE_POLL_V2_TIMESTAMP;
}
public static long getIssueAssetV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return ISSUE_ASSET_V2_TIMESTAMP;
}
public static long getCreateOrderV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return CREATE_ORDER_V2_TIMESTAMP;
}
public static long getArbitraryTransactionV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return ARBITRARY_TRANSACTION_V2_TIMESTAMP;
}
public static long getDeployATV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return DEPLOY_AT_V2_TIMESTAMP;
return QORA_V2_TIMESTAMP;
}
}

View File

@ -10,6 +10,7 @@ import java.util.Map.Entry;
import data.PaymentData;
import data.assets.AssetData;
import data.at.ATData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
@ -59,11 +60,20 @@ public class Payment {
if (!Crypto.isValidAddress(paymentData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
// Do not allow payments to finished/dead ATs
ATData atData = this.repository.getATRepository().fromATAddress(paymentData.getRecipient());
if (atData != null && atData.getIsFinished())
return ValidationResult.AT_IS_FINISHED;
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// If we're sending to an AT then assetId must match AT's assetId
if (atData != null && atData.getAssetId() != paymentData.getAssetId())
return ValidationResult.ASSET_DOES_NOT_MATCH_AT;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;

View File

@ -79,7 +79,8 @@ public class ATTransaction extends Transaction {
amount = amount.subtract(this.atTransactionData.getAmount());
}
if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null)
if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null
&& this.atTransactionData.getAssetId() == Asset.QORA)
amount = amount.add(this.atTransactionData.getAmount());
return amount;
@ -118,7 +119,7 @@ public class ATTransaction extends Transaction {
return ValidationResult.INVALID_AT_TRANSACTION;
// If we have no payment then we're done
if (amount == null)
if (amountIsZero)
return ValidationResult.OK;
// Check amount is zero or positive

View File

@ -12,6 +12,7 @@ import org.ciyam.at.MachineState;
import com.google.common.base.Utf8;
import data.assets.AssetData;
import data.transaction.DeployATTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
@ -157,6 +158,16 @@ public class DeployATTransaction extends Transaction {
if (deployATTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
long assetId = deployATTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && deployATTransactionData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Check fee is positive
if (deployATTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
@ -168,9 +179,19 @@ public class DeployATTransaction extends Transaction {
return ValidationResult.INVALID_REFERENCE;
// Check creator has enough funds
BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount());
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
return ValidationResult.NO_BALANCE;
if (assetId == Asset.QORA) {
// Simple case: amount and fee both in Qora
BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount());
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
return ValidationResult.NO_BALANCE;
} else {
if (creator.getConfirmedBalance(Asset.QORA).compareTo(deployATTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId).compareTo(deployATTransactionData.getAmount()) < 0)
return ValidationResult.NO_BALANCE;
}
// Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) {
@ -199,9 +220,11 @@ public class DeployATTransaction extends Transaction {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
long assetId = deployATTransactionData.getAssetId();
// Update creator's balance
Account creator = getCreator();
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getAmount()));
creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).subtract(deployATTransactionData.getAmount()));
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getFee()));
// Update creator's reference
@ -212,7 +235,7 @@ public class DeployATTransaction extends Transaction {
atAccount.setLastReference(deployATTransactionData.getSignature());
// Update AT's balance
atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount());
atAccount.setConfirmedBalance(assetId, deployATTransactionData.getAmount());
}
@Override
@ -224,15 +247,17 @@ public class DeployATTransaction extends Transaction {
// Delete this transaction itself
this.repository.getTransactionRepository().delete(deployATTransactionData);
long assetId = deployATTransactionData.getAssetId();
// Update creator's balance
Account creator = getCreator();
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getAmount()));
creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).add(deployATTransactionData.getAmount()));
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getFee()));
// Update creator's reference
creator.setLastReference(deployATTransactionData.getReference());
// Delete AT's account
// Delete AT's account (and hence its balance)
this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress());
}

View File

@ -120,7 +120,7 @@ public class IssueAssetTransaction extends Transaction {
return ValidationResult.NO_BALANCE;
// Check the asset name isn't already taken. This check is not present in gen1.
if (issueAssetTransactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
if (issueAssetTransactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName()))
return ValidationResult.ASSET_ALREADY_EXISTS;

View File

@ -101,6 +101,8 @@ public abstract class Transaction {
INVALID_TAGS_LENGTH(37),
INVALID_AT_TYPE_LENGTH(38),
INVALID_AT_TRANSACTION(39),
AT_IS_FINISHED(40),
ASSET_DOES_NOT_MATCH_AT(41),
ASSET_ALREADY_EXISTS(43),
NOT_YET_RELEASED(1000);
@ -269,8 +271,10 @@ public abstract class Transaction {
public static int getVersionByTimestamp(long timestamp) {
if (timestamp < BlockChain.getPowFixReleaseTimestamp()) {
return 1;
} else {
} else if (timestamp < BlockChain.getQoraV2Timestamp()) {
return 3;
} else {
return 4;
}
}

View File

@ -14,6 +14,7 @@ public interface TransactionRepository {
public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException;
/** Returns block height containing transaction or 0 if not in a block or transaction doesn't exist */
public int getHeightFromSignature(byte[] signature) throws DataException;
@Deprecated

View File

@ -26,7 +26,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATData fromATAddress(String atAddress) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, creation, version, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?",
"SELECT creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?",
atAddress)) {
if (resultSet == null)
return null;
@ -34,23 +34,24 @@ public class HSQLDBATRepository implements ATRepository {
byte[] creatorPublicKey = resultSet.getBytes(1);
long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
int version = resultSet.getInt(3);
byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB
boolean isSleeping = resultSet.getBoolean(5);
long assetId = resultSet.getLong(4);
byte[] codeBytes = resultSet.getBytes(5); // XXX: Actually BLOB
boolean isSleeping = resultSet.getBoolean(6);
Integer sleepUntilHeight = resultSet.getInt(6);
Integer sleepUntilHeight = resultSet.getInt(7);
if (resultSet.wasNull())
sleepUntilHeight = null;
boolean isFinished = resultSet.getBoolean(7);
boolean hadFatalError = resultSet.getBoolean(8);
boolean isFrozen = resultSet.getBoolean(9);
boolean isFinished = resultSet.getBoolean(8);
boolean hadFatalError = resultSet.getBoolean(9);
boolean isFrozen = resultSet.getBoolean(10);
BigDecimal frozenBalance = resultSet.getBigDecimal(10);
BigDecimal frozenBalance = resultSet.getBigDecimal(11);
if (resultSet.wasNull())
frozenBalance = null;
return new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
frozenBalance);
return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError,
isFrozen, frozenBalance);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
@ -61,7 +62,7 @@ public class HSQLDBATRepository implements ATRepository {
List<ATData> executableATs = new ArrayList<ATData>();
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT AT_address, creator, creation, version, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC")) {
"SELECT AT_address, creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC")) {
if (resultSet == null)
return executableATs;
@ -72,22 +73,23 @@ public class HSQLDBATRepository implements ATRepository {
byte[] creatorPublicKey = resultSet.getBytes(2);
long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
int version = resultSet.getInt(4);
byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB
boolean isSleeping = resultSet.getBoolean(6);
long assetId = resultSet.getLong(5);
byte[] codeBytes = resultSet.getBytes(6); // XXX: Actually BLOB
boolean isSleeping = resultSet.getBoolean(7);
Integer sleepUntilHeight = resultSet.getInt(7);
Integer sleepUntilHeight = resultSet.getInt(8);
if (resultSet.wasNull())
sleepUntilHeight = null;
boolean hadFatalError = resultSet.getBoolean(8);
boolean isFrozen = resultSet.getBoolean(9);
boolean hadFatalError = resultSet.getBoolean(9);
boolean isFrozen = resultSet.getBoolean(10);
BigDecimal frozenBalance = resultSet.getBigDecimal(10);
BigDecimal frozenBalance = resultSet.getBigDecimal(11);
if (resultSet.wasNull())
frozenBalance = null;
ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
frozenBalance);
ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
executableATs.add(atData);
} while (resultSet.next());
@ -117,9 +119,10 @@ public class HSQLDBATRepository implements ATRepository {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", new Timestamp(atData.getCreation()))
.bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()).bind("is_sleeping", atData.getIsSleeping())
.bind("sleep_until_height", atData.getSleepUntilHeight()).bind("is_finished", atData.getIsFinished())
.bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()).bind("frozen_balance", atData.getFrozenBalance());
.bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()).bind("code_bytes", atData.getCodeBytes())
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
.bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
.bind("frozen_balance", atData.getFrozenBalance());
try {
saveHelper.execute(this.repository);

View File

@ -222,7 +222,7 @@ public class HSQLDBDatabaseUpdates {
break;
case 12:
// Arbitrary/Multi-payment Transaction Payments
// Arbitrary/Multi-payment/Message/Payment Transaction Payments
stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QoraAddress NOT NULL, "
+ "amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, "
+ "PRIMARY KEY (signature, recipient, asset_id), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
@ -277,7 +277,7 @@ public class HSQLDBDatabaseUpdates {
// Deploy CIYAM AT Transactions
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QoraPublicKey NOT NULL, AT_name ATName NOT NULL, "
+ "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, "
+ "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, "
+ "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QoraAddress, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
// For looking up the Deploy AT Transaction based on deployed AT address
stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)");
@ -360,7 +360,7 @@ public class HSQLDBDatabaseUpdates {
// CIYAM Automated Transactions
stmt.execute(
"CREATE TABLE ATs (AT_address QoraAddress, creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
+ "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ "asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
+ "PRIMARY key (AT_address))");
// For finding executable ATs, ordered by creation timestamp

View File

@ -18,7 +18,8 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT AT_name, description, AT_type, AT_tags, creation_bytes, amount, AT_address FROM DeployATTransactions WHERE signature = ?", signature)) {
"SELECT AT_name, description, AT_type, AT_tags, creation_bytes, amount, asset_id, AT_address FROM DeployATTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
@ -28,14 +29,15 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi
String tags = resultSet.getString(4);
byte[] creationBytes = resultSet.getBytes(5);
BigDecimal amount = resultSet.getBigDecimal(6).setScale(8);
long assetId = resultSet.getLong(7);
// Special null-checking for AT address
String ATAddress = resultSet.getString(7);
String ATAddress = resultSet.getString(8);
if (resultSet.wasNull())
ATAddress = null;
return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference,
signature);
return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp,
reference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch deploy AT transaction from repository", e);
}
@ -51,7 +53,7 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi
.bind("AT_name", deployATTransactionData.getName()).bind("description", deployATTransactionData.getDescription())
.bind("AT_type", deployATTransactionData.getATType()).bind("AT_tags", deployATTransactionData.getTags())
.bind("creation_bytes", deployATTransactionData.getCreationBytes()).bind("amount", deployATTransactionData.getAmount())
.bind("AT_address", deployATTransactionData.getATAddress());
.bind("asset_id", deployATTransactionData.getAssetId()).bind("AT_address", deployATTransactionData.getATAddress());
try {
saveHelper.execute(this.repository);

View File

@ -5,7 +5,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

View File

@ -2,6 +2,7 @@ package transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.json.simple.JSONObject;
@ -23,7 +24,8 @@ public class ATTransactionTransformer extends TransactionTransformer {
private static final int ASSET_ID_LENGTH = LONG_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + DATA_SIZE_LENGTH;
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH
+ DATA_SIZE_LENGTH;
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
throw new TransformationException("Serialized AT Transactions should not exist!");
@ -49,7 +51,8 @@ public class ATTransactionTransformer extends TransactionTransformer {
Serialization.serializeAddress(bytes, atTransactionData.getRecipient());
if (atTransactionData.getAssetId() != null) {
// Only emit amount if greater than zero (safer than checking assetId)
if (atTransactionData.getAmount().compareTo(BigDecimal.ZERO) > 0) {
Serialization.serializeBigDecimal(bytes, atTransactionData.getAmount());
bytes.write(Longs.toByteArray(atTransactionData.getAssetId()));
}

View File

@ -50,7 +50,6 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
// V3+ allows payments but always return a list of payments, even if empty
List<PaymentData> payments = new ArrayList<PaymentData>();
;
if (version != 1) {
int paymentsCount = byteBuffer.getInt();
@ -133,7 +132,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (arbitraryTransactionData.getVersion() == 1 || transactionData.getTimestamp() >= BlockChain.getArbitraryTransactionV2Timestamp())
if (arbitraryTransactionData.getVersion() == 1 || transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return bytes;
// Special v1 version

View File

@ -90,7 +90,7 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer {
* @throws TransformationException
*/
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
if (transactionData.getTimestamp() >= BlockChain.getCreateOrderV2Timestamp())
if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return TransactionTransformer.toBytesForSigningImpl(transactionData);
// Special v1 version

View File

@ -62,7 +62,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
pollOptions.add(new PollOptionData(optionName));
// V1 only: voter count also present
if (timestamp < BlockChain.getCreatePollV2Timestamp()) {
if (timestamp < BlockChain.getQoraV2Timestamp()) {
int voterCount = byteBuffer.getInt();
if (voterCount != 0)
throw new TransformationException("Unexpected voter count in byte data for CreatePollTransaction");
@ -88,7 +88,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
// option-string-length, option-string
dataLength += INT_LENGTH + Utf8.encodedLength(pollOptionData.getOptionName());
if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp())
if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp())
// v1 only: voter-count (should always be zero)
dataLength += INT_LENGTH;
}
@ -120,7 +120,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
for (PollOptionData pollOptionData : pollOptions) {
Serialization.serializeSizedString(bytes, pollOptionData.getOptionName());
if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp()) {
if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) {
// In v1, CreatePollTransaction uses Poll.toBytes which serializes voters too.
// Zero voters as this is a new poll.
bytes.write(Ints.toByteArray(0));
@ -149,7 +149,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (transactionData.getTimestamp() >= BlockChain.getCreatePollV2Timestamp())
if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return bytes;
// Special v1 version

View File

@ -14,6 +14,7 @@ import com.google.common.primitives.Longs;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.block.BlockChain;
import qora.transaction.DeployATTransaction;
import data.transaction.DeployATTransactionData;
@ -30,13 +31,17 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
private static final int TAGS_SIZE_LENGTH = INT_LENGTH;
private static final int CREATION_BYTES_SIZE_LENGTH = INT_LENGTH;
private static final int AMOUNT_LENGTH = LONG_LENGTH;
private static final int ASSET_ID_LENGTH = LONG_LENGTH;
private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + AT_TYPE_SIZE_LENGTH
+ TAGS_SIZE_LENGTH + CREATION_BYTES_SIZE_LENGTH + AMOUNT_LENGTH;
private static final int V4_TYPELESS_LENGTH = TYPELESS_LENGTH + ASSET_ID_LENGTH;
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int version = DeployATTransaction.getVersionByTimestamp(timestamp);
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
@ -59,20 +64,34 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
long assetId = Asset.QORA;
if (version >= 4)
assetId = byteBuffer.getLong();
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, signature);
return new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference,
signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
int dataLength = TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(deployATTransactionData.getName())
+ Utf8.encodedLength(deployATTransactionData.getDescription()) + Utf8.encodedLength(deployATTransactionData.getATType())
+ Utf8.encodedLength(deployATTransactionData.getTags()) + deployATTransactionData.getCreationBytes().length;
int dataLength = TYPE_LENGTH;
int version = DeployATTransaction.getVersionByTimestamp(transactionData.getTimestamp());
if (version >= 4)
dataLength += V4_TYPELESS_LENGTH;
else
dataLength += TYPELESS_LENGTH;
dataLength += Utf8.encodedLength(deployATTransactionData.getName()) + Utf8.encodedLength(deployATTransactionData.getDescription())
+ Utf8.encodedLength(deployATTransactionData.getATType()) + Utf8.encodedLength(deployATTransactionData.getTags())
+ deployATTransactionData.getCreationBytes().length;
return dataLength;
}
@ -81,6 +100,8 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
try {
DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
int version = DeployATTransaction.getVersionByTimestamp(transactionData.getTimestamp());
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(deployATTransactionData.getType().value));
@ -103,6 +124,9 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
if (version >= 4)
bytes.write(Longs.toByteArray(deployATTransactionData.getAssetId()));
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
if (deployATTransactionData.getSignature() != null)
@ -115,20 +139,19 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
}
/**
* In Qora v1, the bytes used for verification omit AT-type and tags so we need to test for v1-ness and adjust the bytes
* accordingly.
* In Qora v1, the bytes used for verification omit AT-type and tags so we need to test for v1-ness and adjust the bytes accordingly.
*
* @param transactionData
* @return byte[]
* @throws TransformationException
*/
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
if (transactionData.getTimestamp() >= BlockChain.getDeployATV2Timestamp())
if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return TransactionTransformer.toBytesForSigningImpl(transactionData);
// Special v1 version
// Easier to start from scratch
// Easier to start from scratch
try {
DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
@ -179,6 +202,7 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
json.put("tags", deployATTransactionData.getTags());
json.put("creationBytes", HashCode.fromBytes(deployATTransactionData.getCreationBytes()).toString());
json.put("amount", deployATTransactionData.getAmount().toPlainString());
json.put("assetId", deployATTransactionData.getAssetId());
} catch (ClassCastException e) {
throw new TransformationException(e);
}

View File

@ -55,7 +55,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
byte[] assetReference = new byte[ASSET_REFERENCE_LENGTH];
// In v1, IssueAssetTransaction uses Asset.parse which also deserializes reference.
if (timestamp < BlockChain.getIssueAssetV2Timestamp())
if (timestamp < BlockChain.getQoraV2Timestamp())
byteBuffer.get(assetReference);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
@ -73,7 +73,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
+ Utf8.encodedLength(issueAssetTransactionData.getDescription());
// In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference.
if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp())
if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp())
dataLength += ASSET_REFERENCE_LENGTH;
return dataLength;
@ -100,7 +100,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0));
// In v1, IssueAssetTransaction uses Asset.toBytes which also serializes Asset's reference which is the IssueAssetTransaction's signature
if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) {
if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) {
byte[] assetReference = issueAssetTransactionData.getSignature();
if (assetReference != null)
bytes.write(assetReference);
@ -130,7 +130,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return bytes;
// Special v1 version

View File

@ -99,7 +99,7 @@ public class MultiPaymentTransactionTransformer extends TransactionTransformer {
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp())
return bytes;
// Special v1 version

View File

@ -0,0 +1,332 @@
package test;
import java.io.File;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionBroadcast;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/**
* Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT.
*
* Initiator (wants Qora, has BTC)
* Funds BTC P2SH address
*
* Responder (has Qora, wants BTC)
* Builds Qora ACCT AT and funds it with Qora
*
* Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
*
* Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT
* (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?)
*
* Qora ACCT AT sends its Qora to initiator
*
*/
public class BTCACCTTests {
private static final long TIMEOUT = 600L;
private static final Coin sendValue = Coin.valueOf(6_000L);
private static final Coin fee = Coin.valueOf(2_000L);
private static final byte[] senderPrivKeyBytes = HashCode.fromString("027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c").asBytes();
private static final byte[] recipientPrivKeyBytes = HashCode.fromString("ec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03").asBytes();
// The following need to be updated manually
private static final String prevTxHash = "70ee97f20afea916c2e7b47f6abf3c75f97c4c2251b4625419406a2dd47d16b5";
private static final Coin prevTxBalance = Coin.valueOf(562_000L); // This is NOT the amount but the unspent balance
private static final long prevTxOutputIndex = 1L;
// For when we want to re-run
private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes();
private static final long prevLockTime = 1539347892L;
private static final boolean usePreviousFundingTx = true;
private static final boolean doRefundNotRedeem = false;
@BeforeClass
public static void beforeClass() {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
}
@Test
public void buildBTCACCTTest() throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret);
if (usePreviousFundingTx)
secret = prevSecret;
System.out.println("Secret: " + HashCode.fromBytes(secret).toString());
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
String secretHashHex = HashCode.fromBytes(secretHash).toString();
System.out.println("SHA256(secret): " + secretHashHex);
NetworkParameters params = TestNet3Params.get();
// NetworkParameters params = RegTestParams.get();
System.out.println("Network: " + params.getId());
WalletAppKit kit = new WalletAppKit(params, new File("."), "btc-tests");
kit.setBlockingStartup(false);
kit.startAsync();
kit.awaitRunning();
long now = System.currentTimeMillis() / 1000L;
long lockTime = now + TIMEOUT;
if (usePreviousFundingTx)
lockTime = prevLockTime;
System.out.println("LockTime: " + lockTime);
ECKey senderKey = ECKey.fromPrivate(senderPrivKeyBytes);
kit.wallet().importKey(senderKey);
ECKey recipientKey = ECKey.fromPrivate(recipientPrivKeyBytes);
kit.wallet().importKey(recipientKey);
byte[] senderPubKey = senderKey.getPubKey();
System.out.println("Sender address: " + senderKey.toAddress(params).toBase58());
System.out.println("Sender pubkey: " + HashCode.fromBytes(senderPubKey).toString());
byte[] recipientPubKey = recipientKey.getPubKey();
System.out.println("Recipient address: " + recipientKey.toAddress(params).toBase58());
System.out.println("Recipient pubkey: " + HashCode.fromBytes(recipientPubKey).toString());
byte[] redeemScriptBytes = buildRedeemScript(secret, senderPubKey, recipientPubKey, lockTime);
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
byte[] redeemScriptHash = hash160(redeemScriptBytes);
Address p2shAddress = Address.fromP2SHHash(params, redeemScriptHash);
System.out.println("P2SH address: " + p2shAddress.toBase58());
// Send amount to P2SH address
Transaction fundingTransaction = buildFundingTransaction(params, Sha256Hash.wrap(prevTxHash), prevTxOutputIndex, prevTxBalance, senderKey,
sendValue.add(fee), redeemScriptHash);
System.out.println("Sending " + sendValue.add(fee).toPlainString() + " to " + p2shAddress.toBase58());
if (!usePreviousFundingTx)
broadcastWithConfirmation(kit, fundingTransaction);
if (doRefundNotRedeem) {
// Refund
System.out.println("Refunding " + sendValue.toPlainString() + " back to " + senderKey.toAddress(params));
now = System.currentTimeMillis() / 1000L;
long refundLockTime = now - 60 * 30; // 30 minutes in the past, needs to before 'now' and before "median block time" (median of previous 11 block
// timestamps)
if (refundLockTime < lockTime)
throw new RuntimeException("Too soon to refund");
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction refundTransaction = buildRefundTransaction(params, fundingOutPoint, senderKey, sendValue, redeemScriptBytes, refundLockTime);
broadcastWithConfirmation(kit, refundTransaction);
} else {
// Redeem
System.out.println("Redeeming " + sendValue.toPlainString() + " to " + recipientKey.toAddress(params));
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction redeemTransaction = buildRedeemTransaction(params, fundingOutPoint, recipientKey, sendValue, secret, redeemScriptBytes);
broadcastWithConfirmation(kit, redeemTransaction);
}
kit.wallet().cleanup();
for (Transaction transaction : kit.wallet().getTransactionPool(Pool.PENDING).values())
System.out.println("Pending tx: " + transaction.getHashAsString());
}
private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes();
private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes();
private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes();
private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes();
private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes();
private byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
try {
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
byte[] senderPubKeyHash = hash160(senderPubKey);
byte[] recipientPubKeyHash = hash160(recipientPubKey);
return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash, redeemScript3, toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript4, senderPubKeyHash, redeemScript5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private byte[] hash160(byte[] input) {
try {
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
return rmd160Digester.digest(sha256Digester.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
byte[] redeemScriptHash) {
Transaction fundingTransaction = new Transaction(params);
// Outputs (needed before input so inputs can be signed)
// Fixed amount to P2SH
fundingTransaction.addOutput(value, ScriptBuilder.createP2SHOutputScript(redeemScriptHash));
// Change to sender
fundingTransaction.addOutput(balance.minus(value).minus(fee), ScriptBuilder.createOutputScript(sigKey.toAddress(params)));
// Input
// We create fake "to address" scriptPubKey for prev tx so our spending input is P2PKH type
Script fakeScriptPubKey = ScriptBuilder.createOutputScript(sigKey.toAddress(params));
TransactionOutPoint prevOut = new TransactionOutPoint(params, outputIndex, prevTxHash);
fundingTransaction.addSignedInput(prevOut, fakeScriptPubKey, sigKey);
return fundingTransaction;
}
private Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
byte[] redeemScriptBytes) {
Transaction redeemTransaction = new Transaction(params);
redeemTransaction.setVersion(2);
// Outputs
redeemTransaction.addOutput(value, ScriptBuilder.createOutputScript(recipientKey.toAddress(params)));
// Input
byte[] recipientPubKey = recipientKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0xffffffffL); // Final
redeemTransaction.addInput(input);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = redeemTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("redeem transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = recipientKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("redeem transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return redeemTransaction;
}
private Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
byte[] redeemScriptBytes, long lockTime) {
Transaction refundTransaction = new Transaction(params);
refundTransaction.setVersion(2);
// Outputs
refundTransaction.addOutput(value, ScriptBuilder.createOutputScript(senderKey.toAddress(params)));
// Input
byte[] recipientPubKey = senderKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0);
refundTransaction.addInput(input);
// Set locktime after input but before input signature is generated
refundTransaction.setLockTime(lockTime);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = refundTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("refund transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = senderKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("refund transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return refundTransaction;
}
private void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
System.out.println("Broadcasting tx: " + transaction.getHashAsString());
System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString());
System.out.println("Number of connected peers: " + kit.peerGroup().numConnectedPeers());
TransactionBroadcast txBroadcast = kit.peerGroup().broadcastTransaction(transaction);
try {
txBroadcast.future().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Transaction broadcast failed", e);
}
// wait for confirmation
System.out.println("Waiting for confirmation of tx: " + transaction.getHashAsString());
try {
transaction.getConfidence().getDepthFuture(1).get();
} catch (CancellationException | ExecutionException | InterruptedException e) {
throw new RuntimeException("Transaction confirmation failed", e);
}
System.out.println("Confirmed tx: " + transaction.getHashAsString());
}
/** Convert int to little-endian byte array */
private byte[] toLEByteArray(int value) {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
}
}

57
tests/test/BTCTests.java Normal file
View File

@ -0,0 +1,57 @@
package test;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.junit.Test;
import com.google.common.hash.HashCode;
import crosschain.BTC;
public class BTCTests {
@Test
public void testWatchAddress() throws Exception {
// String testAddress = "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M";
String testAddress = "1GRENT17xMQe2ukPhwAeZU1TaUUon1Qc65";
long testStartTime = 1539000000L;
BTC btc = BTC.getInstance();
btc.watch(testAddress, testStartTime);
Thread.sleep(5000);
btc.watch(testAddress, testStartTime);
btc.shutdown();
}
@Test
public void testWatchScript() throws Exception {
long testStartTime = 1539000000L;
BTC btc = BTC.getInstance();
byte[] redeemScriptHash = HashCode.fromString("3dbcc35e69ebc449f616fa3eb3723dfad9cbb5b3").asBytes();
Script redeemScript = ScriptBuilder.createP2SHOutputScript(redeemScriptHash);
redeemScript.setCreationTimeSeconds(testStartTime);
// btc.watch(redeemScript);
Thread.sleep(5000);
// btc.watch(redeemScript);
btc.shutdown();
}
@Test
public void updateCheckpoints() throws Exception {
BTC btc = BTC.getInstance();
btc.updateCheckpoints();
}
}