From ccc3dbd3397c057ce27609012432d589a9af6d06 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Mon, 26 May 2014 16:26:22 +0200 Subject: [PATCH] Implement standard checks for BIP62 shortest possible data push rules. Also fix ScriptBuilder so it doesn't build longer than necessary data pushes any more. --- .../google/bitcoin/script/ScriptBuilder.java | 13 ++++- .../google/bitcoin/script/ScriptChunk.java | 36 ++++++++++++++ .../bitcoin/wallet/DefaultRiskAnalysis.java | 25 +++++++++- .../bitcoin/script/ScriptChunkTest.java | 49 +++++++++++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/com/google/bitcoin/script/ScriptChunkTest.java diff --git a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java index 1f19f710..44e6ab17 100644 --- a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java +++ b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java @@ -48,10 +48,19 @@ public class ScriptBuilder { } public ScriptBuilder data(byte[] data) { + // implements BIP62 byte[] copy = Arrays.copyOf(data, data.length); int opcode; - if (data.length < OP_PUSHDATA1) { - opcode = data.length; // OP_0 in case of empty vector + if (data.length == 0) { + opcode = OP_0; + } else if (data.length == 1) { + byte b = data[0]; + if (b >= 1 && b <= 16) + opcode = Script.encodeToOpN(b); + else + opcode = 1; + } else if (data.length < OP_PUSHDATA1) { + opcode = data.length; } else if (data.length < 256) { opcode = OP_PUSHDATA1; } else if (data.length < 65536) { diff --git a/core/src/main/java/com/google/bitcoin/script/ScriptChunk.java b/core/src/main/java/com/google/bitcoin/script/ScriptChunk.java index 4318e8a8..f624edf4 100644 --- a/core/src/main/java/com/google/bitcoin/script/ScriptChunk.java +++ b/core/src/main/java/com/google/bitcoin/script/ScriptChunk.java @@ -26,6 +26,10 @@ import java.util.Arrays; import javax.annotation.Nullable; import static com.google.bitcoin.core.Utils.bytesToHexString; +import static com.google.bitcoin.script.ScriptOpCodes.OP_0; +import static com.google.bitcoin.script.ScriptOpCodes.OP_1; +import static com.google.bitcoin.script.ScriptOpCodes.OP_16; +import static com.google.bitcoin.script.ScriptOpCodes.OP_1NEGATE; import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA1; import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA2; import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA4; @@ -64,11 +68,43 @@ public class ScriptChunk { return opcode > OP_PUSHDATA4; } + /** + * Returns true if this chunk is pushdata content, including the single-byte pushdatas. + */ + public boolean isPushData() { + return opcode <= OP_16; + } + public int getStartLocationInProgram() { checkState(startLocationInProgram >= 0); return startLocationInProgram; } + /** + * Called on a pushdata chunk, returns true if it uses the smallest possible way (according to BIP62) to push the data. + */ + public boolean isShortestPossiblePushData() { + checkState(isPushData()); + if (data.length == 0) + return opcode == OP_0; + if (data.length == 1) { + byte b = data[0]; + if (b >= 0x01 && b <= 0x10) + return opcode == OP_1 + b - 1; + if (b == 0x81) + return opcode == OP_1NEGATE; + } + if (data.length < OP_PUSHDATA1) + return opcode == data.length; + if (data.length < 256) + return opcode == OP_PUSHDATA1; + if (data.length < 65536) + return opcode == OP_PUSHDATA2; + + // can never be used, but implemented for completeness + return opcode == OP_PUSHDATA4; + } + public void write(OutputStream stream) throws IOException { if (isOpCode()) { checkState(data == null); diff --git a/core/src/main/java/com/google/bitcoin/wallet/DefaultRiskAnalysis.java b/core/src/main/java/com/google/bitcoin/wallet/DefaultRiskAnalysis.java index 6611284a..636db5bc 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DefaultRiskAnalysis.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DefaultRiskAnalysis.java @@ -20,8 +20,11 @@ package com.google.bitcoin.wallet; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.TransactionConfidence; +import com.google.bitcoin.core.TransactionInput; import com.google.bitcoin.core.TransactionOutput; import com.google.bitcoin.core.Wallet; +import com.google.bitcoin.script.ScriptChunk; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +107,8 @@ public class DefaultRiskAnalysis implements RiskAnalysis { public enum RuleViolation { NONE, VERSION, - DUST + DUST, + SHORTEST_POSSIBLE_PUSHDATA } /** @@ -127,6 +131,25 @@ public class DefaultRiskAnalysis implements RiskAnalysis { log.warn("TX considered non-standard due to output {} being dusty", i); return RuleViolation.DUST; } + for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) { + if (chunk.isPushData() && !chunk.isShortestPossiblePushData()) { + log.warn("TX considered non-standard due to output {} having a longer than necessary data push: {}", + i, chunk); + return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; + } + } + } + + final List inputs = tx.getInputs(); + for (int i = 0; i < inputs.size(); i++) { + TransactionInput input = inputs.get(i); + for (ScriptChunk chunk : input.getScriptSig().getChunks()) { + if (chunk.data != null && chunk.isShortestPossiblePushData()) { + log.warn("TX considered non-standard due to input {} having a longer than necessary data push: {}", + i, chunk); + return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; + } + } } return RuleViolation.NONE; diff --git a/core/src/test/java/com/google/bitcoin/script/ScriptChunkTest.java b/core/src/test/java/com/google/bitcoin/script/ScriptChunkTest.java new file mode 100644 index 00000000..90c67cbb --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/script/ScriptChunkTest.java @@ -0,0 +1,49 @@ +/** + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.script; + +import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA1; +import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA2; +import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA4; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ScriptChunkTest { + + @Test + public void testShortestPossibleDataPush() { + assertTrue("empty push", new ScriptBuilder().data(new byte[0]).build().getChunks().get(0) + .isShortestPossiblePushData()); + + for (byte i = -1; i < 127; i++) + assertTrue("push of single byte " + i, new ScriptBuilder().data(new byte[] { i }).build().getChunks() + .get(0).isShortestPossiblePushData()); + + for (int len = 2; len < Script.MAX_SCRIPT_ELEMENT_SIZE; len++) + assertTrue("push of " + len + " bytes", new ScriptBuilder().data(new byte[len]).build().getChunks().get(0) + .isShortestPossiblePushData()); + + // non-standard chunks + for (byte i = 1; i <= 16; i++) + assertFalse("push of smallnum " + i, new ScriptChunk(1, new byte[] { i }).isShortestPossiblePushData()); + assertFalse("push of 75 bytes", new ScriptChunk(OP_PUSHDATA1, new byte[75]).isShortestPossiblePushData()); + assertFalse("push of 255 bytes", new ScriptChunk(OP_PUSHDATA2, new byte[255]).isShortestPossiblePushData()); + assertFalse("push of 65535 bytes", new ScriptChunk(OP_PUSHDATA4, new byte[65535]).isShortestPossiblePushData()); + } +}