mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-12 02:05:53 +00:00
Better support for lock timed transactions.
Lock times are now included in various toString dumps. Transactions can estimate their lock time when the time is specified as a block number. Add support to WalletTool for creating timelocked transactions.
This commit is contained in:
parent
48cdc1d9e7
commit
b5b43f3a15
@ -774,4 +774,19 @@ public abstract class AbstractBlockChain {
|
|||||||
public synchronized boolean isOrphan(Sha256Hash block) {
|
public synchronized boolean isOrphan(Sha256Hash block) {
|
||||||
return orphanBlocks.containsKey(block);
|
return orphanBlocks.containsKey(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an estimate of when the given block will be reached, assuming a perfect 10 minute average for each
|
||||||
|
* block. This is useful for turning transaction lock times into human readable times. Note that a height in
|
||||||
|
* the past will still be estimated, even though the time of solving is actually known (we won't scan backwards
|
||||||
|
* through the chain to obtain the right answer).
|
||||||
|
*/
|
||||||
|
public Date estimateBlockTime(int height) {
|
||||||
|
synchronized (chainHeadLock) {
|
||||||
|
long offset = height - chainHead.getHeight();
|
||||||
|
long headTime = chainHead.getHeader().getTimeSeconds();
|
||||||
|
long estimated = (headTime * 1000) + (1000L * 60L * 10L * offset);
|
||||||
|
return new Date(estimated);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static com.google.bitcoin.core.Utils.*;
|
import static com.google.bitcoin.core.Utils.*;
|
||||||
@ -51,12 +53,8 @@ public class Transaction extends ChildMessage implements Serializable {
|
|||||||
// These are serialized in both bitcoin and java serialization.
|
// These are serialized in both bitcoin and java serialization.
|
||||||
private long version;
|
private long version;
|
||||||
private ArrayList<TransactionInput> inputs;
|
private ArrayList<TransactionInput> inputs;
|
||||||
//a cached copy to prevent constantly rewrapping
|
|
||||||
//private transient List<TransactionInput> immutableInputs;
|
|
||||||
|
|
||||||
private ArrayList<TransactionOutput> outputs;
|
private ArrayList<TransactionOutput> outputs;
|
||||||
//a cached copy to prevent constantly rewrapping
|
|
||||||
//private transient List<TransactionOutput> immutableOutputs;
|
|
||||||
|
|
||||||
private long lockTime;
|
private long lockTime;
|
||||||
|
|
||||||
@ -548,13 +546,31 @@ public class Transaction extends ChildMessage implements Serializable {
|
|||||||
return getConfidence().getDepthInBlocks() >= params.getSpendableCoinbaseDepth();
|
return getConfidence().getDepthInBlocks() >= params.getSpendableCoinbaseDepth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return toString(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A human readable version of the transaction useful for debugging. The format is not guaranteed to be stable.
|
* A human readable version of the transaction useful for debugging. The format is not guaranteed to be stable.
|
||||||
|
* @param chain If provided, will be used to estimate lock times (if set). Can be null.
|
||||||
*/
|
*/
|
||||||
public String toString() {
|
public String toString(AbstractBlockChain chain) {
|
||||||
// Basic info about the tx.
|
// Basic info about the tx.
|
||||||
StringBuffer s = new StringBuffer();
|
StringBuffer s = new StringBuffer();
|
||||||
s.append(String.format(" %s: %s%n", getHashAsString(), getConfidence()));
|
s.append(String.format(" %s: %s%n", getHashAsString(), getConfidence()));
|
||||||
|
if (lockTime > 0) {
|
||||||
|
String time;
|
||||||
|
if (lockTime < LOCKTIME_THRESHOLD) {
|
||||||
|
time = "block " + lockTime;
|
||||||
|
if (chain != null) {
|
||||||
|
time = time + " (estimated to be reached at " +
|
||||||
|
chain.estimateBlockTime((int)lockTime).toString() + ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
time = new Date(lockTime).toString();
|
||||||
|
}
|
||||||
|
s.append(String.format(" time locked until %s%n", time));
|
||||||
|
}
|
||||||
if (inputs.size() == 0) {
|
if (inputs.size() == 0) {
|
||||||
s.append(String.format(" INCOMPLETE: No inputs!%n"));
|
s.append(String.format(" INCOMPLETE: No inputs!%n"));
|
||||||
return s.toString();
|
return s.toString();
|
||||||
@ -1026,4 +1042,28 @@ public class Transaction extends ChildMessage implements Serializable {
|
|||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the string either as a whole number of blocks, or if it contains slashes as a YYYY/MM/DD format date
|
||||||
|
* and returns the lock time in wire format.
|
||||||
|
*/
|
||||||
|
public static long parseLockTimeStr(String lockTimeStr) throws ParseException {
|
||||||
|
if (lockTimeStr.indexOf("/") != -1) {
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd");
|
||||||
|
Date date = format.parse(lockTimeStr);
|
||||||
|
return date.getTime() / 1000;
|
||||||
|
}
|
||||||
|
return Long.parseLong(lockTimeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns either the lock time as a date, if it was specified in seconds, or an estimate based on the time in
|
||||||
|
* the current head block if it was specified as a block time.
|
||||||
|
*/
|
||||||
|
public Date estimateLockTime(AbstractBlockChain chain) {
|
||||||
|
if (lockTime < LOCKTIME_THRESHOLD)
|
||||||
|
return chain.estimateBlockTime((int)getLockTime());
|
||||||
|
else
|
||||||
|
return new Date(getLockTime()*1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1719,10 +1719,17 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized String toString() {
|
public synchronized String toString() {
|
||||||
return toString(false);
|
return toString(false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized String toString(boolean includePrivateKeys) {
|
/**
|
||||||
|
* Formats the wallet as a human readable piece of text. Intended for debugging, the format is not meant to be
|
||||||
|
* stable or human readable.
|
||||||
|
* @param includePrivateKeys Whether raw private key data should be included.
|
||||||
|
* @param chain If set, will be used to estimate lock times for block timelocked transactions.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public synchronized String toString(boolean includePrivateKeys, AbstractBlockChain chain) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(String.format("Wallet containing %s BTC in:%n", bitcoinValueToFriendlyString(getBalance())));
|
builder.append(String.format("Wallet containing %s BTC in:%n", bitcoinValueToFriendlyString(getBalance())));
|
||||||
builder.append(String.format(" %d unspent transactions%n", unspent.size()));
|
builder.append(String.format(" %d unspent transactions%n", unspent.size()));
|
||||||
@ -1743,28 +1750,29 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
// Print the transactions themselves
|
// Print the transactions themselves
|
||||||
if (unspent.size() > 0) {
|
if (unspent.size() > 0) {
|
||||||
builder.append("\nUNSPENT:\n");
|
builder.append("\nUNSPENT:\n");
|
||||||
toStringHelper(builder, unspent);
|
toStringHelper(builder, unspent, chain);
|
||||||
}
|
}
|
||||||
if (spent.size() > 0) {
|
if (spent.size() > 0) {
|
||||||
builder.append("\nSPENT:\n");
|
builder.append("\nSPENT:\n");
|
||||||
toStringHelper(builder, spent);
|
toStringHelper(builder, spent, chain);
|
||||||
}
|
}
|
||||||
if (pending.size() > 0) {
|
if (pending.size() > 0) {
|
||||||
builder.append("\nPENDING:\n");
|
builder.append("\nPENDING:\n");
|
||||||
toStringHelper(builder, pending);
|
toStringHelper(builder, pending, chain);
|
||||||
}
|
}
|
||||||
if (inactive.size() > 0) {
|
if (inactive.size() > 0) {
|
||||||
builder.append("\nINACTIVE:\n");
|
builder.append("\nINACTIVE:\n");
|
||||||
toStringHelper(builder, inactive);
|
toStringHelper(builder, inactive, chain);
|
||||||
}
|
}
|
||||||
if (dead.size() > 0) {
|
if (dead.size() > 0) {
|
||||||
builder.append("\nDEAD:\n");
|
builder.append("\nDEAD:\n");
|
||||||
toStringHelper(builder, dead);
|
toStringHelper(builder, dead, chain);
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toStringHelper(StringBuilder builder, Map<Sha256Hash, Transaction> transactionMap) {
|
private void toStringHelper(StringBuilder builder, Map<Sha256Hash, Transaction> transactionMap,
|
||||||
|
AbstractBlockChain chain) {
|
||||||
for (Transaction tx : transactionMap.values()) {
|
for (Transaction tx : transactionMap.values()) {
|
||||||
try {
|
try {
|
||||||
builder.append("Sends ");
|
builder.append("Sends ");
|
||||||
@ -1777,7 +1785,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
} catch (ScriptException e) {
|
} catch (ScriptException e) {
|
||||||
// Ignore and don't print this line.
|
// Ignore and don't print this line.
|
||||||
}
|
}
|
||||||
builder.append(tx);
|
builder.append(tx.toString(chain));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import static com.google.bitcoin.core.TestUtils.createFakeBlock;
|
import static com.google.bitcoin.core.TestUtils.createFakeBlock;
|
||||||
import static com.google.bitcoin.core.TestUtils.createFakeTx;
|
import static com.google.bitcoin.core.TestUtils.createFakeTx;
|
||||||
@ -360,4 +362,13 @@ public class BlockChainTest {
|
|||||||
b1.verifyHeader();
|
b1.verifyHeader();
|
||||||
return b1;
|
return b1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void estimatedBlockTime() throws Exception {
|
||||||
|
NetworkParameters params = NetworkParameters.prodNet();
|
||||||
|
BlockChain prod = new BlockChain(params, new MemoryBlockStore(params));
|
||||||
|
Date d = prod.estimateBlockTime(200000);
|
||||||
|
// The actual date of block 200,000 was 2012-09-22 10:47:00
|
||||||
|
assertEquals(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2012/10/23 17:35:05"), d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import java.io.IOException;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@ -86,9 +87,13 @@ public class WalletTool {
|
|||||||
" --action=SEND Creates a transaction with the given --output from this wallet and broadcasts, eg:\n" +
|
" --action=SEND Creates a transaction with the given --output from this wallet and broadcasts, eg:\n" +
|
||||||
" --output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245\n" +
|
" --output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245\n" +
|
||||||
" You can repeat --output=address:value multiple times.\n" +
|
" You can repeat --output=address:value multiple times.\n" +
|
||||||
" If the output destination starts with 04 and is 65 bytes (130 chars) it will be\n" +
|
" If the output destination starts with 04 and is 65 or 33 bytes long it will be\n" +
|
||||||
" treated as a public key instead of an address and the send will use \n" +
|
" treated as a public key instead of an address and the send will use \n" +
|
||||||
" <key> CHECKSIG as the script. You can also specify a --fee=0.01\n" +
|
" <key> CHECKSIG as the script.\n" +
|
||||||
|
" Other options include:\n" +
|
||||||
|
" --fee=0.01 sets the tx fee\n" +
|
||||||
|
" --locktime=1234 sets the lock time to block 1234\n" +
|
||||||
|
" --locktime=2013/01/01 sets the lock time to 1st Jan 2013\n" +
|
||||||
|
|
||||||
"\n>>> WAITING\n" +
|
"\n>>> WAITING\n" +
|
||||||
"You can wait for the condition specified by the --waitfor flag to become true. Transactions and new\n" +
|
"You can wait for the condition specified by the --waitfor flag to become true. Transactions and new\n" +
|
||||||
@ -242,6 +247,7 @@ public class WalletTool {
|
|||||||
parser.accepts("value").withRequiredArg();
|
parser.accepts("value").withRequiredArg();
|
||||||
parser.accepts("fee").withRequiredArg();
|
parser.accepts("fee").withRequiredArg();
|
||||||
conditionFlag = parser.accepts("condition").withRequiredArg();
|
conditionFlag = parser.accepts("condition").withRequiredArg();
|
||||||
|
parser.accepts("locktime").withRequiredArg();
|
||||||
options = parser.parse(args);
|
options = parser.parse(args);
|
||||||
|
|
||||||
if (args.length == 0 || options.has("help") || options.nonOptionArguments().size() > 0) {
|
if (args.length == 0 || options.has("help") || options.nonOptionArguments().size() > 0) {
|
||||||
@ -348,7 +354,11 @@ public class WalletTool {
|
|||||||
if (options.has("fee")) {
|
if (options.has("fee")) {
|
||||||
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
|
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
|
||||||
}
|
}
|
||||||
send(outputFlag.values(options), fee);
|
String lockTime = null;
|
||||||
|
if (options.has("locktime")) {
|
||||||
|
lockTime = (String) options.valueOf("locktime");
|
||||||
|
}
|
||||||
|
send(outputFlag.values(options), fee, lockTime);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +370,15 @@ public class WalletTool {
|
|||||||
saveWallet(walletFile);
|
saveWallet(walletFile);
|
||||||
|
|
||||||
if (options.has(waitForFlag)) {
|
if (options.has(waitForFlag)) {
|
||||||
wait(waitForFlag.value(options));
|
WaitForEnum value;
|
||||||
|
try {
|
||||||
|
value = waitForFlag.value(options);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Could not understand the --waitfor flag: Valid options are WALLET_TX, BLOCK, " +
|
||||||
|
"BALANCE and EVER");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wait(value);
|
||||||
if (!wallet.isConsistent()) {
|
if (!wallet.isConsistent()) {
|
||||||
System.err.println("************** WALLET IS INCONSISTENT *****************");
|
System.err.println("************** WALLET IS INCONSISTENT *****************");
|
||||||
return;
|
return;
|
||||||
@ -370,7 +388,7 @@ public class WalletTool {
|
|||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void send(List<String> outputs, BigInteger fee) {
|
private static void send(List<String> outputs, BigInteger fee, String lockTimeStr) {
|
||||||
try {
|
try {
|
||||||
// Convert the input strings to outputs.
|
// Convert the input strings to outputs.
|
||||||
Transaction t = new Transaction(params);
|
Transaction t = new Transaction(params);
|
||||||
@ -383,7 +401,7 @@ public class WalletTool {
|
|||||||
String destination = parts[0];
|
String destination = parts[0];
|
||||||
try {
|
try {
|
||||||
BigInteger value = Utils.toNanoCoins(parts[1]);
|
BigInteger value = Utils.toNanoCoins(parts[1]);
|
||||||
if (destination.startsWith("04") && destination.length() == 130) {
|
if (destination.startsWith("04") && (destination.length() == 130 || destination.length() == 66)) {
|
||||||
// Treat as a raw public key.
|
// Treat as a raw public key.
|
||||||
BigInteger pubKey = new BigInteger(destination, 16);
|
BigInteger pubKey = new BigInteger(destination, 16);
|
||||||
ECKey key = new ECKey(null, pubKey);
|
ECKey key = new ECKey(null, pubKey);
|
||||||
@ -409,6 +427,16 @@ public class WalletTool {
|
|||||||
System.err.println("Insufficient funds: have " + wallet.getBalance());
|
System.err.println("Insufficient funds: have " + wallet.getBalance());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (lockTimeStr != null) {
|
||||||
|
t.setLockTime(Transaction.parseLockTimeStr(lockTimeStr));
|
||||||
|
// For lock times to take effect, at least one output must have a non-final sequence number.
|
||||||
|
t.getInputs().get(0).setSequenceNumber(0);
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
System.err.println("Could not understand --locktime of " + lockTimeStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
t = req.tx; // Not strictly required today.
|
t = req.tx; // Not strictly required today.
|
||||||
setup();
|
setup();
|
||||||
peers.startAndWait();
|
peers.startAndWait();
|
||||||
@ -661,7 +689,8 @@ public class WalletTool {
|
|||||||
wallet.keychain.remove(key);
|
wallet.keychain.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void dumpWallet() {
|
private static void dumpWallet() throws BlockStoreException {
|
||||||
System.out.println(wallet.toString(true));
|
setup(); // To get the chain height so we can estimate lock times.
|
||||||
|
System.out.println(wallet.toString(true, chain));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user