diff --git a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java index b79d77a5..697193bb 100644 --- a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java @@ -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); + } + } } diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java index a13c6b9d..823400af 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -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 inputs; - //a cached copy to prevent constantly rewrapping - //private transient List immutableInputs; private ArrayList outputs; - //a cached copy to prevent constantly rewrapping - //private transient List 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); + } } diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 57812fbc..95d81acb 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -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 transactionMap) { + private void toStringHelper(StringBuilder builder, Map 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)); } } diff --git a/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java b/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java index 7dca96a6..9f1642d7 100644 --- a/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java +++ b/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java @@ -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); + } } diff --git a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java index d6d1bf36..e6c20de5 100644 --- a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java +++ b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java @@ -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" + - " CHECKSIG as the script. You can also specify a --fee=0.01\n" + + " 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 outputs, BigInteger fee) { + private static void send(List 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)); } }