CIYAM AT & cross-chain trading.

Bump CIYAM AT requirement to v1.3

Remove multi-blockchain AT aspect for now (BlockchainAPI).

For PUT_PREVIOUS_BLOCK_HASH_INTO_A we no longer use SHA256 to condense 64-byte block signature into 32 bytes.
Now we put block height into A1 and SHA192 of signature into A2 through A4.
This allows possible future lookup of block data using "block hash", with verification that it is the same block.

Some AT functions use "address in B" but sometimes we populate B with account's public key instead.
So the method "getAccountFromB" is smart and checks for an actual, textual address in B starting with 'Q', otherwise assumes B contains public key.

The Settings field "useBitcoinTestNet" (boolean) now replaced with "bitcoinNet" (String) with possible values MAIN (default), TEST3, REGTEST.
This allows for more varied development/testing scenarios.

Use correct Bitcoin nSequence value 0xFFFFFFFE for P2SH, i.e. enable locktime, disable RBF.

Roll REGTEST checkpoints file generator into main BTC class.

Yet another rewrite of Bitcoin P2SH scripts for BTC-QORT cross-chain trading.
Added associated test classes BuildP2SH, CheckP2SH, DeployAT (unfinished).
This commit is contained in:
catbref
2020-04-08 09:29:25 +01:00
parent 8844cc0076
commit 87bb9090f5
19 changed files with 943 additions and 461 deletions

View File

@@ -1,153 +0,0 @@
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

@@ -1,7 +1,8 @@
package org.qora.crosschain;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.function.Function;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
@@ -11,6 +12,7 @@ import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
@@ -26,124 +28,149 @@ public class BTCACCT {
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
private static final byte[] redeemScript1 = HashCode.fromString("76a914").asBytes(); // OP_DUP OP_HASH160 push(0x14 bytes)
private static final byte[] redeemScript2 = HashCode.fromString("88ada97614").asBytes(); // OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
private static final byte[] redeemScript3 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
private static final byte[] redeemScript4 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
/*
* OP_TUCK (to copy public key to before signature)
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
* OP_HASH160 (convert public key to PKH)
* OP_DUP (duplicate PKH)
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
* OP_IF
* OP_DROP (no need for duplicate PKH)
* <push 4 bytes> <locktime>
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
* OP_ELSE
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
* OP_HASH160 (hash secret)
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
* OP_ENDIF
*/
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeem script.
* <p>
* <pre>
* OP_DUP OP_HASH160 push(0x14) &lt;trade pubkeyhash&gt; OP_EQUALVERIFY OP_CHECKSIGVERIFY
* OP_HASH160 OP_DUP push(0x14) &lt;sender/refund P2PKH&gt; OP_EQUAL
* OP_TUCK OP_CHECKSIGVERIFY
* OP_HASH160 OP_DUP push(0x14) &lt;refunder pubkeyhash&gt; OP_EQUAL
* OP_IF
* OP_DROP push(0x04 bytes) &lt;refund locktime&gt; OP_CHECKLOCKTIMEVERIFY
* OP_ELSE
* push(0x14) &lt;redeemer P2PKH&gt; OP_EQUAL
* push(0x14) &lt;redeemer pubkeyhash&gt; OP_EQUALVERIFY
* OP_HASH160 push(0x14 bytes) &lt;hash of secret&gt; OP_EQUAL
* OP_ENDIF
* </pre>
*
* @param tradePubKeyHash
* @param refunderPubKeyHash
* @param senderPubKey
* @param recipientPubKey
* @param lockTime
* @return
*/
public static byte[] buildScript(byte[] tradePubKeyHash, byte[] senderPubKeyHash, byte[] recipientPubKeyHash, int lockTime) {
return Bytes.concat(redeemScript1, tradePubKeyHash, redeemScript2, senderPubKeyHash, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript4, recipientPubKeyHash, redeemScript5);
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
}
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey tradeKey, byte[] senderPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
/**
* Builds a custom transaction to spend P2SH.
*
* @param amount
* @param spendKey
* @param recipientPubKeyHash
* @param fundingOutput
* @param redeemScriptBytes
* @param lockTime
* @param scriptSigBuilder
* @return
*/
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction refundTransaction = new Transaction(params);
refundTransaction.setVersion(2);
Transaction transaction = new Transaction(params);
transaction.setVersion(2);
// Output is back to P2SH funder
ECKey senderKey = ECKey.fromPublicOnly(senderPubKey);
refundTransaction.addOutput(refundAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
refundTransaction.addInput(input);
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
else
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
transaction.addInput(input);
// Set locktime after inputs added but before input signatures are generated
refundTransaction.setLockTime(lockTime);
if (lockTime != null)
transaction.setLockTime(lockTime);
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = refundTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// sender/refund pubkey
scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey));
// transaction signature
// Calculate transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin();
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// trade public key
byte[] tradePubKey = tradeKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
/// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
// Build scriptSig using lambda and tx signature
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
// Set input scriptSig
refundTransaction.getInput(0).setScriptSig(scriptBuilder.build());
transaction.getInput(0).setScriptSig(scriptSig);
return refundTransaction;
return transaction;
}
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey tradeKey, byte[] recipientPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
Transaction redeemTransaction = new Transaction(params);
redeemTransaction.setVersion(2);
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// Output to redeem recipient
ECKey senderKey = ECKey.fromPublicOnly(recipientPubKey);
redeemTransaction.addOutput(redeemAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
// redeem public key
byte[] refundPubKey = refundKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
redeemTransaction.addInput(input);
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = redeemTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
return scriptBuilder.build();
};
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// recipient pubkey
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
// transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin();
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// trade public key
byte[] tradePubKey = tradeKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
/// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
// Set input scriptSig
redeemTransaction.getInput(0).setScriptSig(scriptBuilder.build());
return redeemTransaction;
return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder);
}
public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) {
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// secret
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// redeem public key
byte[] redeemPubKey = redeemKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
return scriptBuilder.build();
};
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
}
public static byte[] buildQortalAT(byte[] secretHash, String destinationQortalAddress, long refundMinutes, BigDecimal initialPayout) {
// Labels for data segment addresses
int addrCounter = 0;
final int addrHashPart1 = addrCounter++;
@@ -155,6 +182,9 @@ public class BTCACCT {
final int addrAddressPart3 = addrCounter++;
final int addrAddressPart4 = addrCounter++;
final int addrRefundMinutes = addrCounter++;
final int addrHashTempIndex = addrCounter++;
final int addrHashTempLength = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
final int addrRefundTimestamp = addrCounter++;
final int addrLastTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
@@ -164,19 +194,30 @@ public class BTCACCT {
final int addrAddressTemp2 = addrCounter++;
final int addrAddressTemp3 = addrCounter++;
final int addrAddressTemp4 = addrCounter++;
final int addrHashTemp1 = addrCounter++;
final int addrHashTemp2 = addrCounter++;
final int addrHashTemp3 = addrCounter++;
final int addrHashTemp4 = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8);
// Hash of secret into HashPart1-4
dataByteBuffer.put(secretHash);
// Destination Qortal account's public key
dataByteBuffer.put(destinationQortalPubKey);
dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0));
// Expiry in minutes
dataByteBuffer.putLong(refundMinutes);
// Temp buffer for hashing any passed secret
dataByteBuffer.putLong(addrHashTemp1);
dataByteBuffer.putLong(32L);
// Initial payout amount
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue());
// Code labels
final int addrTxLoop = 0x36;
final int addrCheckTx = 0x4b;
@@ -187,13 +228,17 @@ public class BTCACCT {
final int addrEndOfCode = 0x109;
int tempPC;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1);
// init:
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp);
codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp);
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
.putInt(addrRefundTimestamp).putInt(addrRefundMinutes);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount);
codeByteBuffer.put(OpCode.SET_PCS.value);
// loop:
@@ -204,7 +249,7 @@ public class BTCACCT {
// txloop:
assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect";
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_IN_A.value).putInt(addrLastTimestamp);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(addrLastTimestamp);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC));
@@ -220,10 +265,7 @@ public class BTCACCT {
// checkSender
assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect";
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B1.value).putInt(addrAddressTemp1);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B2.value).putInt(addrAddressTemp2);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B3.value).putInt(addrAddressTemp3);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B4.value).putInt(addrAddressTemp4);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrAddressTemp1);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position();
@@ -236,23 +278,16 @@ public class BTCACCT {
// checkMessage:
assert codeByteBuffer.position() == addrCheckMessage : "addrCheckMessage incorrect";
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrHashPart1);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrHashPart2);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrHashPart3);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrHashPart4);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(addrComparator);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrHashTempIndex).putInt(addrHashTempLength);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC));
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
// payout:
assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect";
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrAddressPart1);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrAddressPart2);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrAddressPart3);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrAddressPart4);
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MESSAGE_A_TO_ADDRESS_IN_B.value);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
codeByteBuffer.put(OpCode.FIN_IMD.value);

View File

@@ -17,19 +17,24 @@ import org.qortal.account.Account;
import org.qortal.account.GenesisAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.CiyamAtSettings;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction;
import com.google.common.primitives.Bytes;
@@ -108,31 +113,90 @@ public class QortalATAPI extends API {
}
@Override
public void putPreviousBlockHashInA(MachineState state) {
public void putPreviousBlockHashIntoA(MachineState state) {
try {
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1;
// We only need signature, so only request a block summary
List<BlockSummaryData> blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight);
if (blockSummaries == null || blockSummaries.size() != 1)
throw new RuntimeException("AT API unable to fetch previous block hash?");
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
byte[] blockHash = Crypto.digest(blockData.getSignature());
// To be able to use hash to look up block, save height (8 bytes) and rehash with SHA192 (24 bytes)
this.setA1(state, previousBlockHeight);
this.setA(state, blockHash);
byte[] sigHash192 = sha192(blockSummaries.get(0).getSignature());
this.setA2(state, fromBytes(sigHash192, 0));
this.setA3(state, fromBytes(sigHash192, 8));
this.setA4(state, fromBytes(sigHash192, 16));
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
}
@Override
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
// Recipient is this AT
String recipient = this.atData.getATAddress();
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
BlockRepository blockRepository = this.getRepository().getBlockRepository();
try {
Account recipientAccount = new Account(this.getRepository(), recipient);
int currentHeight = blockRepository.getBlockchainHeight();
while (height <= currentHeight) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(this.getRepository(), blockData);
List<Transaction> blockTransactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= blockTransactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = blockTransactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
// Found a transaction
this.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[] sigHash192 = sha192(transaction.getTransactionData().getSignature());
this.setA2(state, fromBytes(sigHash192, 0));
this.setA3(state, fromBytes(sigHash192, 8));
this.setA4(state, fromBytes(sigHash192, 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);
}
}
@Override
public long getTypeFromTransactionInA(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
switch (transactionData.getType()) {
case PAYMENT:
@@ -154,9 +218,23 @@ public class QortalATAPI extends API {
@Override
public long getAmountFromTransactionInA(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
TransactionData transactionData = this.getTransactionFromA(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();
// fall-through to default
default:
return 0xffffffffffffffffL;
}
}
@Override
@@ -168,8 +246,8 @@ public class QortalATAPI extends API {
@Override
public long generateRandomUsingTransactionInA(MachineState state) {
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
// value.
// The plan here is to sleep for a block then use next block's signature
// and this transaction's signature to generate pseudo-random, but deterministic, value.
if (!isFirstOpCodeAfterSleeping(state)) {
// First call
@@ -182,7 +260,7 @@ public class QortalATAPI extends API {
// Second call
// HASH(A and new block hash)
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
try {
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
@@ -206,7 +284,7 @@ public class QortalATAPI extends API {
// Zero B in case of issues or shorter-than-B message
this.zeroB(state);
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
byte[] messageData = null;
@@ -236,7 +314,7 @@ public class QortalATAPI extends API {
@Override
public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = transactionData.getCreatorPublicKey();
@@ -265,9 +343,7 @@ public class QortalATAPI extends API {
@Override
public void payAmountToB(long unscaledAmount, MachineState state) {
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
Account recipient = getAccountFromB(state);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
@@ -285,9 +361,7 @@ public class QortalATAPI extends API {
@Override
public void messageAToB(MachineState state) {
byte[] message = state.getA();
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
Account recipient = getAccountFromB(state);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
@@ -306,7 +380,7 @@ public class QortalATAPI extends API {
int blockHeight = timestamp.blockHeight;
// At least one block in the future
blockHeight += (minutes / this.ciyamAtSettings.minutesPerBlock) + 1;
blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1);
return new Timestamp(blockHeight, 0).longValue();
}
@@ -380,7 +454,7 @@ public class QortalATAPI extends API {
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
/* package */ TransactionData fetchTransaction(MachineState state) {
/* package */ TransactionData getTransactionFromA(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
try {
@@ -415,11 +489,6 @@ public class QortalATAPI extends API {
* Timestamp is block's timestamp + position in AT-Transactions list.
*
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
* issue.
*
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
*/
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
@@ -443,4 +512,27 @@ public class QortalATAPI extends API {
}
}
/**
* Returns Account (possibly PublicKeyAccount) based on value in B.
* <p>
* If bytes in B start with 'Q' then use B as an address, but only if valid.
* <p>
* Otherwise, assume B is a public key.
* @return
*/
private Account getAccountFromB(MachineState state) {
byte[] bBytes = state.getB();
if (bBytes[0] == 'Q') {
int zeroIndex = Bytes.indexOf(bBytes, (byte) 0);
if (zeroIndex > 0) {
String address = new String(bBytes, 0, zeroIndex);
if (Crypto.isValidAddress(address))
return new Account(this.repository, address);
}
}
return new PublicKeyAccount(this.repository, bBytes);
}
}

View File

@@ -1,6 +1,5 @@
package org.qortal.at;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
@@ -10,6 +9,7 @@ import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.crosschain.BTC;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
@@ -20,52 +20,6 @@ import org.qortal.settings.Settings;
*
*/
public enum QortalFunctionCode {
/**
* <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.
*/
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);
String recipient = new String(state.getB(), StandardCharsets.UTF_8);
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.
@@ -90,7 +44,7 @@ public enum QortalFunctionCode {
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;
byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
convertAddressInB(addressPrefix, state);
}

View File

@@ -10,8 +10,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -19,8 +22,9 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.TreeMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -46,16 +50,19 @@ 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.MonetaryFormat;
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;
public class BTC {
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
private static final MessageDigest RIPE_MD160_DIGESTER;
private static final MessageDigest SHA256_DIGESTER;
static {
@@ -69,6 +76,29 @@ public class BTC {
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
public enum BitcoinNet {
MAIN {
@Override
public NetworkParameters getParams() {
return MainNetParams.get();
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return TestNet3Params.get();
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return RegTestParams.get();
}
};
public abstract NetworkParameters getParams();
}
private static BTC instance;
private final NetworkParameters params;
@@ -85,24 +115,60 @@ public class BTC {
private static final String MINIMAL_TESTNET3_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
private static final String MINIMAL_MAINNET_TEXTFILE = "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, File checkpointsFile) throws IOException {
super(params, getMinimalTextFileStream(params, checkpointsFile));
}
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
super(params, inputStream);
}
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params, File checkpointsFile) {
if (params == MainNetParams.get())
return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes());
if (params == TestNet3Params.get())
return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes());
if (params == RegTestParams.get())
return newRegTestCheckpointsStream(checkpointsFile); // We have to build this
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
}
private static ByteArrayInputStream newRegTestCheckpointsStream(File checkpointsFile) {
try {
final NetworkParameters params = RegTestParams.get();
final BlockStore store = new MemoryBlockStore(params);
final BlockChain chain = new BlockChain(params, store);
final PeerGroup peerGroup = new PeerGroup(params, chain);
final InetAddress ipAddress = InetAddress.getLoopbackAddress();
final PeerAddress peerAddress = new PeerAddress(params, ipAddress);
peerGroup.addAddress(peerAddress);
peerGroup.start();
final TreeMap<Integer, StoredBlock> checkpoints = new TreeMap<>();
chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block));
peerGroup.downloadBlockChain();
peerGroup.stop();
saveAsText(checkpointsFile, checkpoints.values());
return new ByteArrayInputStream(Files.readAllBytes(checkpointsFile.toPath()));
} catch (BlockStoreException e) {
throw new RuntimeException(e);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void notifyNewBestBlock(StoredBlock block) {
final int height = block.getHeight();
@@ -119,22 +185,22 @@ public class BTC {
this.checkpoints.put(blockTimestamp, block);
try {
this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()));
saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()), this.checkpoints.values());
} catch (FileNotFoundException e) {
// Save failed - log it but it's not critical
LOGGER.warn("Failed to save updated BTC checkpoints: " + e.getMessage());
}
}
public void saveAsText(File textFile) throws FileNotFoundException {
private static void saveAsText(File textFile, Collection<StoredBlock> checkpointBlocks) 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(this.checkpoints.size());
writer.println(checkpointBlocks.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : this.checkpoints.values()) {
for (StoredBlock block : checkpointBlocks) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
@@ -173,16 +239,24 @@ public class BTC {
// Constructors and instance
private BTC() {
if (Settings.getInstance().useBitcoinTestNet()) {
/*
this.params = RegTestParams.get();
this.checkpointsFileName = "checkpoints-regtest.txt";
*/
this.params = TestNet3Params.get();
this.checkpointsFileName = "checkpoints-testnet.txt";
} else {
this.params = MainNetParams.get();
this.checkpointsFileName = "checkpoints.txt";
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
this.params = bitcoinNet.getParams();
switch (bitcoinNet) {
case MAIN:
this.checkpointsFileName = "checkpoints.txt";
break;
case TEST3:
this.checkpointsFileName = "checkpoints-testnet.txt";
break;
case REGTEST:
this.checkpointsFileName = "checkpoints-regtest.txt";
break;
default:
throw new IllegalStateException("Unsupported Bitcoin network: " + bitcoinNet.name());
}
this.directory = new File("Qortal-BTC");
@@ -196,7 +270,7 @@ public class BTC {
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
this.manager = new UpdateableCheckpointManager(this.params);
this.manager = new UpdateableCheckpointManager(this.params, checkpointsFile);
} catch (IOException e2) {
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
}
@@ -222,7 +296,7 @@ public class BTC {
return this.checkpointsFileName;
}
/* package */ NetworkParameters getNetworkParameters() {
public NetworkParameters getNetworkParameters() {
return this.params;
}

View File

@@ -20,6 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.block.BlockChain;
import org.qortal.crosschain.BTC.BitcoinNet;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@@ -91,7 +92,7 @@ public class Settings {
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
private boolean useBitcoinTestNet = false;
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
// Repository related
/** Queries that take longer than this are logged. (milliseconds) */
@@ -345,8 +346,8 @@ public class Settings {
return this.blockchainConfig;
}
public boolean useBitcoinTestNet() {
return this.useBitcoinTestNet;
public BitcoinNet getBitcoinNet() {
return this.bitcoinNet;
}
public Long getSlowQueryThreshold() {