forked from Qortal/qortal
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:
parent
eaad565765
commit
90094be95a
3
.settings/org.eclipse.jdt.apt.core.prefs
Normal file
3
.settings/org.eclipse.jdt.apt.core.prefs
Normal file
@ -0,0 +1,3 @@
|
||||
eclipse.preferences.version=1
|
||||
org.eclipse.jdt.apt.aptEnabled=true
|
||||
org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
304
src/crosschain/BTC.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
134
src/qora/at/BlockchainAPI.java
Normal file
134
src/qora/at/BlockchainAPI.java
Normal 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);
|
||||
|
||||
}
|
@ -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();
|
||||
|
107
src/qora/at/QoraFunctionCode.java
Normal file
107
src/qora/at/QoraFunctionCode.java
Normal 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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
332
tests/test/BTCACCTTests.java
Normal file
332
tests/test/BTCACCTTests.java
Normal 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
57
tests/test/BTCTests.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user