3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-30 23:02:15 +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:
Mike Hearn 2013-01-12 00:35:55 +01:00
parent 48cdc1d9e7
commit b5b43f3a15
5 changed files with 125 additions and 22 deletions

View File

@ -774,4 +774,19 @@ public abstract class AbstractBlockChain {
public synchronized boolean isOrphan(Sha256Hash 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);
}
}
}

View File

@ -23,6 +23,8 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
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.
private long version;
private ArrayList<TransactionInput> inputs;
//a cached copy to prevent constantly rewrapping
//private transient List<TransactionInput> immutableInputs;
private ArrayList<TransactionOutput> outputs;
//a cached copy to prevent constantly rewrapping
//private transient List<TransactionOutput> immutableOutputs;
private long lockTime;
@ -548,13 +546,31 @@ public class Transaction extends ChildMessage implements Serializable {
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.
* @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.
StringBuffer s = new StringBuffer();
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) {
s.append(String.format(" INCOMPLETE: No inputs!%n"));
return s.toString();
@ -1026,4 +1042,28 @@ public class Transaction extends ChildMessage implements Serializable {
return false;
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);
}
}

View File

@ -1719,10 +1719,17 @@ public class Wallet implements Serializable, BlockChainListener {
@Override
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();
builder.append(String.format("Wallet containing %s BTC in:%n", bitcoinValueToFriendlyString(getBalance())));
builder.append(String.format(" %d unspent transactions%n", unspent.size()));
@ -1743,28 +1750,29 @@ public class Wallet implements Serializable, BlockChainListener {
// Print the transactions themselves
if (unspent.size() > 0) {
builder.append("\nUNSPENT:\n");
toStringHelper(builder, unspent);
toStringHelper(builder, unspent, chain);
}
if (spent.size() > 0) {
builder.append("\nSPENT:\n");
toStringHelper(builder, spent);
toStringHelper(builder, spent, chain);
}
if (pending.size() > 0) {
builder.append("\nPENDING:\n");
toStringHelper(builder, pending);
toStringHelper(builder, pending, chain);
}
if (inactive.size() > 0) {
builder.append("\nINACTIVE:\n");
toStringHelper(builder, inactive);
toStringHelper(builder, inactive, chain);
}
if (dead.size() > 0) {
builder.append("\nDEAD:\n");
toStringHelper(builder, dead);
toStringHelper(builder, dead, chain);
}
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()) {
try {
builder.append("Sends ");
@ -1777,7 +1785,7 @@ public class Wallet implements Serializable, BlockChainListener {
} catch (ScriptException e) {
// Ignore and don't print this line.
}
builder.append(tx);
builder.append(tx.toString(chain));
}
}

View File

@ -24,6 +24,8 @@ import org.junit.Before;
import org.junit.Test;
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.createFakeTx;
@ -360,4 +362,13 @@ public class BlockChainTest {
b1.verifyHeader();
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);
}
}

View File

@ -38,6 +38,7 @@ import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
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" +
" --output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245\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" +
" <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" +
"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("fee").withRequiredArg();
conditionFlag = parser.accepts("condition").withRequiredArg();
parser.accepts("locktime").withRequiredArg();
options = parser.parse(args);
if (args.length == 0 || options.has("help") || options.nonOptionArguments().size() > 0) {
@ -348,7 +354,11 @@ public class WalletTool {
if (options.has("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;
}
@ -360,7 +370,15 @@ public class WalletTool {
saveWallet(walletFile);
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()) {
System.err.println("************** WALLET IS INCONSISTENT *****************");
return;
@ -370,7 +388,7 @@ public class WalletTool {
shutdown();
}
private static void send(List<String> outputs, BigInteger fee) {
private static void send(List<String> outputs, BigInteger fee, String lockTimeStr) {
try {
// Convert the input strings to outputs.
Transaction t = new Transaction(params);
@ -383,7 +401,7 @@ public class WalletTool {
String destination = parts[0];
try {
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.
BigInteger pubKey = new BigInteger(destination, 16);
ECKey key = new ECKey(null, pubKey);
@ -409,6 +427,16 @@ public class WalletTool {
System.err.println("Insufficient funds: have " + wallet.getBalance());
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.
setup();
peers.startAndWait();
@ -661,7 +689,8 @@ public class WalletTool {
wallet.keychain.remove(key);
}
private static void dumpWallet() {
System.out.println(wallet.toString(true));
private static void dumpWallet() throws BlockStoreException {
setup(); // To get the chain height so we can estimate lock times.
System.out.println(wallet.toString(true, chain));
}
}