GetTransaction test app to demo fetching any bitcoin transaction using bitcoinj. Plus some AT-API work

This commit is contained in:
catbref 2020-03-27 18:24:41 +00:00
parent 2c4bad6455
commit 8844cc0076
6 changed files with 390 additions and 13 deletions

View File

@ -0,0 +1,153 @@
package org.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 org.qora.account.Account;
import org.qora.block.Block;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.ATTransactionData;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.BlockRepository;
import org.qora.repository.DataException;
import org.qora.transaction.Transaction;
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();
BlockRepository blockRepository = api.getRepository().getBlockRepository();
try {
Account recipientAccount = new Account(api.getRepository(), recipient);
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.getRepository(), 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;
}
}
@Override
public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) {
// TODO
return null;
}
},
BTC(1) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
return 0;
}
@Override
public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) {
// TODO
return null;
}
};
public static class TransactionOutput {
byte[] recipient;
long amount;
}
public final int value;
private static final 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);
public abstract TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex);
}

View File

@ -10,6 +10,8 @@ import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
/**
* Qortal-specific CIYAM-AT Functions.
@ -20,7 +22,7 @@ import org.ciyam.at.Timestamp;
public enum QortalFunctionCode {
/**
* <tt>0x0500</tt><br>
* Returns current BTC block's "timestamp"
* Returns current BTC block's "timestamp".
*/
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
@Override
@ -30,7 +32,7 @@ public enum QortalFunctionCode {
},
/**
* <tt>0x0501</tt><br>
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
* Put transaction from specific recipient after timestamp in A, or zero if none.
*/
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
@Override
@ -42,13 +44,74 @@ public enum QortalFunctionCode {
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
}
},
/**
* <tt>0x0502</tt><br>
* Get output, using transaction in A and passed index, putting address in B and returning amount.<br>
* Return -1 if no such output;
*/
GET_INDEXED_OUTPUT(0x0502, 1, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
int outputIndex = (int) (functionData.value1 & 0xffffffffL);
BlockchainAPI.TransactionOutput output = BlockchainAPI.BTC.getIndexedOutputFromTransactionInA(state, outputIndex);
if (output == null) {
functionData.returnValue = -1L;
return;
}
state.getAPI().setB(state, output.recipient);
functionData.returnValue = output.amount;
}
},
/**
* <tt>0x0510</tt><br>
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
*/
CONVERT_B_TO_PKH(0x0510, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Needs to be 'B' sized
byte[] pkh = new byte[32];
// Copy PKH part of B to last 20 bytes
System.arraycopy(state.getB(), 32 - 20 - 4, pkh, 32 - 20, 20);
state.getAPI().setB(state, pkh);
}
},
/**
* <tt>0x0511</tt><br>
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
* P2SH stored in lower 25 bytes of B.
*/
CONVERT_B_TO_P2SH(0x0511, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
byte addressPrefix = Settings.getInstance().useBitcoinTestNet() ? (byte) 0xc4 : 0x05;
convertAddressInB(addressPrefix, state);
}
},
/**
* <tt>0x0512</tt><br>
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
* Qortal address stored in lower 25 bytes of B.
*/
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
convertAddressInB(Crypto.ADDRESS_VERSION, state);
}
};
public final short value;
public final int paramCount;
public final boolean returnsValue;
private final static Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
private static final Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
private QortalFunctionCode(int value, int paramCount, boolean returnsValue) {
@ -100,4 +163,19 @@ public enum QortalFunctionCode {
/** Actually execute function */
protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
private static void convertAddressInB(byte addressPrefix, MachineState state) {
byte[] addressNoChecksum = new byte[1 + 20];
addressNoChecksum[0] = addressPrefix;
System.arraycopy(state.getB(), 0, addressNoChecksum, 1, 20);
byte[] checksum = Crypto.doubleDigest(addressNoChecksum);
// Needs to be 'B' sized
byte[] address = new byte[32];
System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length);
System.arraycopy(checksum, 0, address, 32 - 4, 4);
state.getAPI().setB(state, address);
}
}

View File

@ -10,7 +10,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
@ -21,6 +20,7 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -28,22 +28,28 @@ 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.PeerAddress;
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.listeners.BlocksDownloadedEventListener;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.MemoryBlockStore;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletTransaction;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
import org.qortal.settings.Settings;
@ -168,10 +174,12 @@ public class BTC {
private BTC() {
if (Settings.getInstance().useBitcoinTestNet()) {
/*
this.params = RegTestParams.get();
this.checkpointsFileName = "checkpoints-regtest.txt";
// TestNet3Params.get();
// this.checkpointsFileName = "checkpoints-testnet.txt";
*/
this.params = TestNet3Params.get();
this.checkpointsFileName = "checkpoints-testnet.txt";
} else {
this.params = MainNetParams.get();
this.checkpointsFileName = "checkpoints.txt";
@ -256,7 +264,25 @@ public class BTC {
return Wallet.createBasic(this.params);
}
private void replayChain(long startTime, Wallet wallet) throws BlockStoreException {
private class ReplayHooks {
private Runnable preReplay;
private Runnable postReplay;
public ReplayHooks(Runnable preReplay, Runnable postReplay) {
this.preReplay = preReplay;
this.postReplay = postReplay;
}
public void preReplay() {
this.preReplay.run();
}
public void postReplay() {
this.postReplay.run();
}
}
private void replayChain(long startTime, Wallet wallet, ReplayHooks replayHooks) throws BlockStoreException {
this.start(startTime);
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
@ -277,12 +303,18 @@ public class BTC {
}
try {
if (replayHooks != null)
replayHooks.preReplay();
// Sync blockchain using peerGroup, skipping as much as we can before startTime
this.peerGroup.setFastCatchupTimeSecs(startTime);
this.chain.addNewBestBlockListener(Threading.SAME_THREAD, this.manager);
this.peerGroup.downloadBlockChain();
} finally {
// Clean up
if (replayHooks != null)
replayHooks.postReplay();
if (wallet != null) {
wallet.removeCoinsReceivedEventListener(coinsReceivedListener);
wallet.removeCoinsSentEventListener(coinsSentListener);
@ -296,7 +328,7 @@ public class BTC {
}
private void replayChain(long startTime) throws BlockStoreException {
this.replayChain(startTime, null);
this.replayChain(startTime, null, null);
}
// Actual useful methods for use by other classes
@ -334,7 +366,7 @@ public class BTC {
wallet.addWatchedAddress(address, startTime);
try {
replayChain(startTime, wallet);
replayChain(startTime, wallet, null);
// Now that blockchain is up-to-date, return current balance
return wallet.getBalance();
@ -344,13 +376,13 @@ public class BTC {
}
}
public List<TransactionOutput> getUnspentOutputs(String base58Address, long startTime) {
public List<TransactionOutput> getOutputs(String base58Address, long startTime) {
Wallet wallet = createEmptyWallet();
Address address = Address.fromString(this.params, base58Address);
wallet.addWatchedAddress(address, startTime);
try {
replayChain(startTime, wallet);
replayChain(startTime, wallet, null);
// Now that blockchain is up-to-date, return outputs
return wallet.getWatchedOutputs(true);
@ -360,4 +392,54 @@ public class BTC {
}
}
private static class TransactionStorage {
private Transaction transaction;
public void store(Transaction transaction) {
this.transaction = transaction;
}
public Transaction getTransaction() {
return this.transaction;
}
}
public List<TransactionOutput> getOutputs(byte[] txId, long startTime) {
Wallet wallet = createEmptyWallet();
// Add random address to wallet
ECKey fakeKey = new ECKey();
wallet.addWatchedAddress(Address.fromKey(this.params, fakeKey, ScriptType.P2PKH), startTime);
final Sha256Hash txHash = Sha256Hash.wrap(txId);
final TransactionStorage transactionStorage = new TransactionStorage();
final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> {
List<Transaction> transactions = block.getTransactions();
if (transactions == null)
return;
for (Transaction transaction : transactions)
if (transaction.getTxId().equals(txHash)) {
System.out.println(String.format("We downloaded block containing tx!"));
transactionStorage.store(transaction);
}
};
ReplayHooks replayHooks = new ReplayHooks(() -> this.peerGroup.addBlocksDownloadedEventListener(listener), () -> this.peerGroup.removeBlocksDownloadedEventListener(listener));
// Replay chain in the hope it will download transactionId as a dependency
try {
replayChain(startTime, wallet, replayHooks);
Transaction realTx = transactionStorage.getTransaction();
return realTx.getOutputs();
} catch (BlockStoreException e) {
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
return null;
}
}
}

View File

@ -0,0 +1,64 @@
package org.qora.test.btcacct;
import java.security.Security;
import java.util.List;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qora.crosschain.BTC;
import org.qora.settings.Settings;
import com.google.common.hash.HashCode;
public class GetTransaction {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: GetTransaction <bitcoin-tx>"));
System.err.println(String.format("example: GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 (mainnet)"));
System.err.println(String.format("example: GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e (testnet)"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 1 || args.length > 1)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
byte[] transactionId = null;
try {
int argIndex = 0;
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
// Chain replay start time
long startTime = (System.currentTimeMillis() / 1000L) - 14 * 24 * 60 * 60; // 14 days before now, in seconds
// Grab all outputs from transaction
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
if (fundingOutputs == null) {
System.out.println(String.format("Transaction not found"));
return;
}
System.out.println(String.format("Found %d output%s", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %d: %s", fundingOutput.getIndex(), fundingOutput.getValue().toPlainString()));
}
}

View File

@ -167,7 +167,7 @@ public class Redeem {
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
if (fundingOutputs.isEmpty()) {

View File

@ -171,7 +171,7 @@ public class Refund {
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
if (fundingOutputs.isEmpty()) {