From 9e7cb1c951f630000b2d322e5bb2af827b597a10 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Wed, 30 Aug 2017 17:31:52 +0200 Subject: [PATCH] Basic support for version 2 transactions. Rather than considering all version 2 transactions risky, we now look more closely if any of its inputs has a relative lock time. We can't check the relative locks though, because we usually don't have the spent outputs (to know when they were creted). --- .../java/org/bitcoinj/core/Transaction.java | 22 +++++++++++- .../org/bitcoinj/core/TransactionInput.java | 13 +++++++ .../bitcoinj/wallet/DefaultRiskAnalysis.java | 9 ++++- .../wallet/DefaultRiskAnalysisTest.java | 34 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index e961e7e3525..665e70d2f34 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -659,6 +659,9 @@ public String toString(@Nullable AbstractBlockChain chain) { } s.append('\n'); } + if (hasRelativeLockTime()) { + s.append(" has relative lock time\n"); + } if (isOptInFullRBF()) { s.append(" opts into full replace-by-fee\n"); } @@ -702,6 +705,8 @@ public String toString(@Nullable AbstractBlockChain chain) { s.append("\n sequence:").append(Long.toHexString(in.getSequenceNumber())); if (in.isOptInFullRBF()) s.append(", opts into full RBF"); + if (version >=2 && in.hasRelativeLockTime()) + s.append(", has RLT"); } } catch (Exception e) { s.append("[exception: ").append(e.getMessage()).append("]"); @@ -1287,7 +1292,8 @@ public void verify() throws VerificationException { } /** - *

A transaction is time locked if at least one of its inputs is non-final and it has a lock time

+ *

A transaction is time-locked if at least one of its inputs is non-final and it has a lock time. A transaction can + * also have a relative lock time which this method doesn't tell. Use {@link #hasRelativeLockTime()} to find out.

* *

To check if this transaction is final at a given height and time, see {@link Transaction#isFinal(int, long)} *

@@ -1301,6 +1307,20 @@ public boolean isTimeLocked() { return false; } + /** + * A transaction has a relative lock time + * (BIP 68) if it is version 2 or + * higher and at least one of its inputs has its {@link TransactionInput.SEQUENCE_LOCKTIME_DISABLE_FLAG} cleared. + */ + public boolean hasRelativeLockTime() { + if (version < 2) + return false; + for (TransactionInput input : getInputs()) + if (input.hasRelativeLockTime()) + return true; + return false; + } + /** * Returns whether this transaction will opt into the * full replace-by-fee semantics. diff --git a/core/src/main/java/org/bitcoinj/core/TransactionInput.java b/core/src/main/java/org/bitcoinj/core/TransactionInput.java index 4b38e3c8238..ea3ff23a817 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionInput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionInput.java @@ -46,6 +46,11 @@ public class TransactionInput extends ChildMessage { /** Magic sequence number that indicates there is no sequence number. */ public static final long NO_SEQUENCE = 0xFFFFFFFFL; + /** + * BIP68: If this flag set, sequence is NOT interpreted as a relative lock-time. + */ + public static final long SEQUENCE_LOCKTIME_DISABLE_FLAG = 1L << 31; + private static final byte[] EMPTY_ARRAY = new byte[0]; // Magic outpoint index that indicates the input is in fact unconnected. private static final long UNCONNECTED = 0xFFFFFFFFL; @@ -385,6 +390,14 @@ public boolean isOptInFullRBF() { return sequence < NO_SEQUENCE - 1; } + /** + * Returns whether this input, if it belongs to a version 2 (or higher) transaction, has + * relative lock-time enabled. + */ + public boolean hasRelativeLockTime() { + return (sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG) == 0; + } + /** * For a connected transaction, runs the script against the connected pubkey and verifies they are correct. * @throws ScriptException if the script did not verify. diff --git a/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java b/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java index 80714ce9088..941b92044af 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java +++ b/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java @@ -89,6 +89,13 @@ private Result analyzeIsFinal() { return Result.NON_FINAL; } + // Relative time-locked transactions are risky too. We can't check the locks because usually we don't know the + // spent outputs (to know when they were created). + if (tx.hasRelativeLockTime()) { + nonFinal = tx; + return Result.NON_FINAL; + } + if (wallet == null) return null; @@ -133,7 +140,7 @@ public enum RuleViolation { */ public static RuleViolation isStandard(Transaction tx) { // TODO: Finish this function off. - if (tx.getVersion() > 1 || tx.getVersion() < 1) { + if (tx.getVersion() > 2 || tx.getVersion() < 1) { log.warn("TX considered non-standard due to unknown version number {}", tx.getVersion()); return RuleViolation.VERSION; } diff --git a/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java b/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java index 7fdbd291e97..0988538158f 100644 --- a/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java @@ -28,6 +28,7 @@ import java.util.*; +import static com.google.common.base.Preconditions.checkState; import static org.bitcoinj.core.Coin.*; import static org.bitcoinj.script.ScriptOpCodes.*; import static org.junit.Assert.*; @@ -231,4 +232,37 @@ public void optInFullRBF() throws Exception { assertEquals(RiskAnalysis.Result.NON_FINAL, analysis.analyze()); assertEquals(tx, analysis.getNonFinal()); } + + @Test + public void relativeLockTime() throws Exception { + Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); + tx.setVersion(2); + checkState(!tx.hasRelativeLockTime()); + + tx.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE); + DefaultRiskAnalysis analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS); + assertEquals(RiskAnalysis.Result.OK, analysis.analyze()); + + tx.getInput(0).setSequenceNumber(0); + analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS); + assertEquals(RiskAnalysis.Result.NON_FINAL, analysis.analyze()); + assertEquals(tx, analysis.getNonFinal()); + } + + @Test + public void transactionVersions() throws Exception { + Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); + tx.setVersion(1); + DefaultRiskAnalysis analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS); + assertEquals(RiskAnalysis.Result.OK, analysis.analyze()); + + tx.setVersion(2); + analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS); + assertEquals(RiskAnalysis.Result.OK, analysis.analyze()); + + tx.setVersion(3); + analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS); + assertEquals(RiskAnalysis.Result.NON_STANDARD, analysis.analyze()); + assertEquals(tx, analysis.getNonStandard()); + } }