diff --git a/pom.xml b/pom.xml index 86867e10a..483896b0d 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 co.rsk.bitcoinj - 0.14.4-rsk-18_2-SNAPSHOT + 0.14.4-rsk-18 bitcoinj-thin bitcoinj-thin diff --git a/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java b/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java index 7f769d9cb..1a632d2e0 100644 --- a/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java +++ b/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java @@ -1,12 +1,14 @@ package co.rsk.bitcoinj.core; import co.rsk.bitcoinj.crypto.TransactionSignature; +import co.rsk.bitcoinj.script.RedeemScriptParser; +import co.rsk.bitcoinj.script.RedeemScriptParserFactory; +import co.rsk.bitcoinj.script.Script; +import com.google.common.base.Preconditions; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.util.*; +import static com.google.common.base.Preconditions.checkState; public class TransactionWitness { static TransactionWitness empty = new TransactionWitness(0); @@ -66,6 +68,105 @@ public byte[] getScriptBytes() { return pushes.get(pushes.size() - 1); } + /** + replicated logic from {@link Script#getSigInsertionIndex} + * */ + public int getSigInsertionIndex(Sha256Hash sigHash, BtcECKey signingKey) { + int witnessSize = getPushCount(); + int redeemScriptIndex = witnessSize - 1; + byte[] redeemScriptData = getPush(redeemScriptIndex); + Script redeemScript = new Script(redeemScriptData); + RedeemScriptParser redeemScriptParser = RedeemScriptParserFactory.get(redeemScript.getChunks()); + + int sigInsertionIndex = 0; + int keyIndexInRedeem = redeemScriptParser.findKeyInRedeem(signingKey); + + byte[] emptyByte = new byte[]{}; + // the pushes that should have the signatures + // are between first one (empty byte for checkmultisig bug) + // and second to last one (op_notif + redeem script) + for (int i = 1; i < getPushCount() - 1; i ++) { + byte[] push = getPush(i); + Preconditions.checkNotNull(push); + if (!Arrays.equals(push, emptyByte)) { + if (keyIndexInRedeem < redeemScriptParser.findSigInRedeem(push, sigHash)) { + return sigInsertionIndex; + } + + sigInsertionIndex++; + } + } + + return sigInsertionIndex; + } + + /** + replicated logic from {@link Script#getScriptSigWithSignature} + * */ + public TransactionWitness updateWitnessWithSignature(Script outputScript, byte[] signature, int targetIndex) { + int sigsPrefixCount = outputScript.getSigsPrefixCount(); + int sigsSuffixCount = outputScript.getSigsSuffixCount(); + return updateWitnessWithSignature(signature, targetIndex, sigsPrefixCount, sigsSuffixCount); + } + + /** + replicated logic from {@link co.rsk.bitcoinj.script.ScriptBuilder#updateScriptWithSignature} + * */ + private TransactionWitness updateWitnessWithSignature(byte[] signature, int targetIndex, int sigsPrefixCount, int sigsSuffixCount) { + int totalPushes = getPushCount(); + + byte[] emptyByte = new byte[]{}; + // since we fill the signatures in order, checking + // the second to last push is enough to know + // if there's space for new signatures + byte[] secondToLastPush = getPush(totalPushes - sigsSuffixCount - 1); + boolean hasMissingSigs = Arrays.equals(secondToLastPush, emptyByte); + Preconditions.checkArgument(hasMissingSigs, "Witness script is already filled with signatures"); + + List updatedPushes = new ArrayList<>(); + // the signatures appear after the prefix + for (int i = 0; i < sigsPrefixCount; i++) { + byte[] push = getPush(i); + updatedPushes.add(push); + } + + int index = 0; + boolean inserted = false; + // copy existing sigs + for (int i = sigsPrefixCount; i < totalPushes - sigsSuffixCount; i++) { + if (index == targetIndex) { + inserted = true; + updatedPushes.add(signature); + ++index; + } + + byte[] push = getPush(i); + if (!Arrays.equals(push, emptyByte)) { + updatedPushes.add(push); + ++index; + } + } + + // add zeros for missing signatures + while (index < totalPushes - sigsPrefixCount - sigsSuffixCount) { + if (index == targetIndex) { + inserted = true; + updatedPushes.add(signature); + } else { + updatedPushes.add(emptyByte); + } + index++; + } + + // copy the suffix + for (int i = totalPushes - sigsSuffixCount; i < totalPushes; i++) { + byte[] push = getPush(i); + updatedPushes.add(push); + } + + checkState(inserted); + return TransactionWitness.of(updatedPushes); + } @Override public boolean equals(Object otherObject) { diff --git a/src/main/java/co/rsk/bitcoinj/script/RedeemScriptValidator.java b/src/main/java/co/rsk/bitcoinj/script/RedeemScriptValidator.java index 29d708a06..d3a1afaa4 100644 --- a/src/main/java/co/rsk/bitcoinj/script/RedeemScriptValidator.java +++ b/src/main/java/co/rsk/bitcoinj/script/RedeemScriptValidator.java @@ -5,6 +5,10 @@ public class RedeemScriptValidator { + private RedeemScriptValidator() { + // Prevent instantiation + } + protected static boolean isRedeemLikeScript(List chunks) { if (chunks.size() < 4) { return false; @@ -12,9 +16,7 @@ protected static boolean isRedeemLikeScript(List chunks) { ScriptChunk lastChunk = chunks.get(chunks.size() - 1); // A standard multisig redeem script must end in OP_CHECKMULTISIG[VERIFY] - boolean isStandard = lastChunk.isOpCode() && - (lastChunk.equalsOpCode(ScriptOpCodes.OP_CHECKMULTISIG) || - lastChunk.equalsOpCode(ScriptOpCodes.OP_CHECKMULTISIGVERIFY)); + boolean isStandard = lastChunk.isOpCheckMultiSig(); if (isStandard) { return true; } @@ -23,9 +25,7 @@ protected static boolean isRedeemLikeScript(List chunks) { ScriptChunk penultimateChunk = chunks.get(chunks.size() - 2); return lastChunk.isOpCode() && lastChunk.equalsOpCode(ScriptOpCodes.OP_ENDIF) && - penultimateChunk.isOpCode() && - (penultimateChunk.equalsOpCode(ScriptOpCodes.OP_CHECKMULTISIG) || - penultimateChunk.equalsOpCode(ScriptOpCodes.OP_CHECKMULTISIGVERIFY)); + penultimateChunk.isOpCheckMultiSig(); } protected static boolean hasStandardRedeemScriptStructure(List chunks) { @@ -34,24 +34,30 @@ protected static boolean hasStandardRedeemScriptStructure(List chun return false; } - // First chunk must be an OP_N - if (!isOpN(chunks.get(0))) { + // last chunk should be OP_CHECKMULTISIG + int chunksSize = chunks.size(); + ScriptChunk lastChunk = chunks.get(chunksSize - 1); + if (!lastChunk.isOpCheckMultiSig()) { return false; } - // Second to last chunk must be an OP_N opcode too, and there should be - // that many data chunks (keys). - ScriptChunk secondToLastChunk = chunks.get(chunks.size() - 2); - if (!isOpN(secondToLastChunk)) { + // First chunk must be a number for the threshold + ScriptChunk firstChunk = chunks.get(0); + // Second to last chunk must be a number for the keys + int secondToLastChunkIndex = chunksSize - 2; + ScriptChunk secondToLastChunk = chunks.get(secondToLastChunkIndex); + + if (!(firstChunk.isPositiveN() && secondToLastChunk.isPositiveN())) { return false; } - int numKeys = Script.decodeFromOpN(secondToLastChunk.opcode); - if (numKeys < 1 || chunks.size() != numKeys + 3) { // numKeys + M + N + OP_CHECKMULTISIG + int numKeys = secondToLastChunk.decodePositiveN(); + // and there should be numKeys+3 total chunks (keys + OP_M + OP_N + OP_CHECKMULTISIG) + if (chunksSize != numKeys + 3) { return false; } - for (int i = 1; i < chunks.size() - 2; i++) { + for (int i = 1; i < secondToLastChunkIndex; i++) { if (chunks.get(i).isOpCode()) { // Should be the public keys, not op_codes return false; } @@ -59,7 +65,7 @@ protected static boolean hasStandardRedeemScriptStructure(List chun return true; } catch (IllegalStateException e) { - return false; // Not an OP_N opcode. + return false; // Not a number } } @@ -68,10 +74,11 @@ protected static boolean hasP2shErpRedeemScriptStructure(List chunk return false; } - ScriptChunk firstChunk = chunks.get(0); + int opNotifIndex = 0; + boolean hasErpPrefix = chunks.get(opNotifIndex).equalsOpCode(ScriptOpCodes.OP_NOTIF); - boolean hasErpPrefix = firstChunk.opcode == ScriptOpCodes.OP_NOTIF; - boolean hasEndIfOpcode = chunks.get(chunks.size() - 1).equalsOpCode(ScriptOpCodes.OP_ENDIF); + int lastChunkIndex = chunks.size() - 1; + boolean hasEndIfOpcode = chunks.get(lastChunkIndex).equalsOpCode(ScriptOpCodes.OP_ENDIF); if (!hasErpPrefix || !hasEndIfOpcode) { return false; @@ -101,8 +108,7 @@ protected static boolean hasP2shErpRedeemScriptStructure(List chunk return false; } - /*** - * Expected structure: + /* The redeem script structure should be as follows: * OP_NOTIF * OP_M * PUBKEYS...N @@ -119,7 +125,6 @@ protected static boolean hasP2shErpRedeemScriptStructure(List chunk * OP_CHECKMULTISIG * OP_ENDIF */ - // Validate both default and erp federations redeem scripts. // Extract the default PowPeg and the emergency multisig redeemscript chunks List defaultFedRedeemScriptChunks = chunks.subList(1, elseOpcodeIndex); @@ -218,9 +223,4 @@ protected static List removeOpCheckMultisig(Script redeemScript) { // Remove the last chunk, which has CHECKMULTISIG op code return redeemScript.getChunks().subList(0, redeemScript.getChunks().size() - 1); } - - protected static boolean isOpN(ScriptChunk chunk) { - return chunk.isOpCode() && - chunk.opcode >= ScriptOpCodes.OP_1 && chunk.opcode <= ScriptOpCodes.OP_16; - } } diff --git a/src/main/java/co/rsk/bitcoinj/script/Script.java b/src/main/java/co/rsk/bitcoinj/script/Script.java index 23a8a8731..1cdbac714 100644 --- a/src/main/java/co/rsk/bitcoinj/script/Script.java +++ b/src/main/java/co/rsk/bitcoinj/script/Script.java @@ -22,15 +22,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import co.rsk.bitcoinj.core.Address; -import co.rsk.bitcoinj.core.BtcECKey; -import co.rsk.bitcoinj.core.BtcTransaction; -import co.rsk.bitcoinj.core.NetworkParameters; -import co.rsk.bitcoinj.core.ProtocolException; -import co.rsk.bitcoinj.core.ScriptException; -import co.rsk.bitcoinj.core.Sha256Hash; -import co.rsk.bitcoinj.core.UnsafeByteArrayOutputStream; -import co.rsk.bitcoinj.core.Utils; +import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.crypto.TransactionSignature; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; @@ -40,14 +32,7 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,6 +82,9 @@ public enum VerifyFlag { private static final Logger log = LoggerFactory.getLogger(Script.class); public static final long MAX_SCRIPT_ELEMENT_SIZE = 520; // bytes + + /** The maximum size in bytes of a standard witnessScript */ + public static final long MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600; public static final int SIG_SIZE = 75; /** Max number of sigops allowed in a standard p2sh redeem script */ public static final int MAX_P2SH_SIGOPS = 15; @@ -459,19 +447,25 @@ private boolean isErpType(Script redeemScript) { * Returns a copy of the given scriptSig with the signature inserted in the given position. */ public Script getScriptSigWithSignature(Script scriptSig, byte[] sigBytes, int index) { - int sigsPrefixCount = 0; - int sigsSuffixCount = 0; - if (isPayToScriptHash()) { - sigsPrefixCount = 1; // OP_0 * - sigsSuffixCount = 1; - } else if (isSentToMultiSig()) { - sigsPrefixCount = 1; // OP_0 * - } else if (isSentToAddress()) { - sigsSuffixCount = 1; // - } + int sigsPrefixCount = getSigsPrefixCount(); + int sigsSuffixCount = getSigsSuffixCount(); return ScriptBuilder.updateScriptWithSignature(scriptSig, sigBytes, index, sigsPrefixCount, sigsSuffixCount); } + public int getSigsPrefixCount() { + if (isPayToScriptHash() || isSentToMultiSig()) { // OP_0 * || OP_0 * + return 1; + } + return 0; + } + + public int getSigsSuffixCount() { + if (isPayToScriptHash() || isSentToAddress()) { // * || + return 1; + } + return 0; + } + private RedeemScriptParser getRedeemScriptParser() { if (redeemScriptParser == null){ redeemScriptParser = RedeemScriptParserFactory.get(chunks); @@ -554,22 +548,24 @@ private static int getSigOpCount(List chunks, boolean accurate) thr static int decodeFromOpN(int opcode) { checkArgument((opcode == OP_0 || opcode == OP_1NEGATE) || (opcode >= OP_1 && opcode <= OP_16), "decodeFromOpN called on non OP_N opcode"); - if (opcode == OP_0) + if (opcode == OP_0) { return 0; - else if (opcode == OP_1NEGATE) + } else if (opcode == OP_1NEGATE) { return -1; - else + } else { return opcode + 1 - OP_1; + } } static int encodeToOpN(int value) { checkArgument(value >= -1 && value <= 16, "encodeToOpN called for " + value + " which we cannot encode in an opcode."); - if (value == 0) + if (value == 0) { return OP_0; - else if (value == -1) + } else if (value == -1) { return OP_1NEGATE; - else + } else { return value - 1 + OP_1; + } } /** diff --git a/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java b/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java index a2c92dde4..d202e25e4 100644 --- a/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java +++ b/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java @@ -38,7 +38,7 @@ * protocol at a lower level.

*/ public class ScriptBuilder { - private List chunks; + private final List chunks; /** Creates a fresh ScriptBuilder with an empty program. */ public ScriptBuilder() { @@ -47,7 +47,7 @@ public ScriptBuilder() { /** Creates a fresh ScriptBuilder with the given program as the starting point. */ public ScriptBuilder(Script template) { - chunks = new ArrayList(template.getChunks()); + chunks = new ArrayList<>(template.getChunks()); } /** Adds the given chunk to the end of the program */ @@ -63,7 +63,7 @@ public ScriptBuilder addChunk(int index, ScriptChunk chunk) { /** Adds the given list of chunks to the end of the program */ public ScriptBuilder addChunks(List chunks) { - chunks.forEach(chunk -> addChunk(chunk)); + chunks.forEach(this::addChunk); return this; } @@ -80,10 +80,11 @@ public ScriptBuilder op(int index, int opcode) { /** Adds a copy of the given byte array as a data element (i.e. PUSHDATA) at the end of the program. */ public ScriptBuilder data(byte[] data) { - if (data.length == 0) + if (data.length == 0) { return smallNum(0); - else + } else { return data(chunks.size(), data); + } } /** Adds a copy of the given byte array as a data element (i.e. PUSHDATA) at the given index in the program. */ @@ -95,10 +96,11 @@ public ScriptBuilder data(int index, byte[] data) { opcode = OP_0; } else if (data.length == 1) { byte b = data[0]; - if (b >= 1 && b <= 16) + if (b >= 1 && b <= 16) { opcode = Script.encodeToOpN(b); - else + } else { opcode = 1; + } } else if (data.length < OP_PUSHDATA1) { opcode = data.length; } else if (data.length < 256) { @@ -267,13 +269,13 @@ public static Script createInputScript(@Nullable TransactionSignature signature) public static Script createMultiSigOutputScript(int threshold, List pubkeys) { checkArgument(threshold > 0); checkArgument(threshold <= pubkeys.size()); - checkArgument(pubkeys.size() <= 16); // That's the max we can represent with a single opcode. + checkArgument(pubkeys.size() <= 20); // That's the max OP_CHECKMULTISIG allows. ScriptBuilder builder = new ScriptBuilder(); - builder.smallNum(threshold); + builder.number(threshold); for (BtcECKey key : pubkeys) { builder.data(key.getPubKey()); } - builder.smallNum(pubkeys.size()); + builder.number(pubkeys.size()); builder.op(OP_CHECKMULTISIG); return builder.build(); } @@ -430,7 +432,7 @@ public static Script createP2SHP2WSHOutputScript(Script redeemScript) { .number(ScriptOpCodes.OP_0) .data(redeemScriptHash) .build(); - return ScriptBuilder.createP2SHOutputScript(witnessScript); + return createP2SHOutputScript(witnessScript); } /** diff --git a/src/main/java/co/rsk/bitcoinj/script/ScriptChunk.java b/src/main/java/co/rsk/bitcoinj/script/ScriptChunk.java index 14d4463b5..a8a42d841 100644 --- a/src/main/java/co/rsk/bitcoinj/script/ScriptChunk.java +++ b/src/main/java/co/rsk/bitcoinj/script/ScriptChunk.java @@ -17,16 +17,17 @@ package co.rsk.bitcoinj.script; +import static co.rsk.bitcoinj.script.ScriptOpCodes.*; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.isNull; + import co.rsk.bitcoinj.core.Utils; import com.google.common.base.Objects; - -import javax.annotation.Nullable; import java.io.IOException; import java.io.OutputStream; +import java.math.BigInteger; import java.util.Arrays; - -import static com.google.common.base.Preconditions.checkState; -import static co.rsk.bitcoinj.script.ScriptOpCodes.*; +import javax.annotation.Nullable; /** * A script element that is either a data push (signature, pubkey, etc) or a non-push (logic, numeric, etc) operation. @@ -40,7 +41,7 @@ public class ScriptChunk { */ @Nullable public final byte[] data; - private int startLocationInProgram; + private final int startLocationInProgram; public ScriptChunk(int opcode, byte[] data) { this(opcode, data, -1); @@ -76,7 +77,7 @@ public int getStartLocationInProgram() { } /** If this chunk is an OP_N opcode returns the equivalent integer value. */ - public int decodeOpN() { + private int decodeOpN() { checkState(isOpCode()); return Script.decodeFromOpN(opcode); } @@ -86,23 +87,30 @@ public int decodeOpN() { */ public boolean isShortestPossiblePushData() { checkState(isPushData()); - if (data == null) + if (data == null) { return true; // OP_N - if (data.length == 0) + } + if (data.length == 0) { return opcode == OP_0; + } if (data.length == 1) { byte b = data[0]; - if (b >= 0x01 && b <= 0x10) + if (b >= 0x01 && b <= 0x10) { return opcode == OP_1 + b - 1; - if ((b & 0xFF) == 0x81) + } + if ((b & 0xFF) == 0x81) { return opcode == OP_1NEGATE; + } } - if (data.length < OP_PUSHDATA1) + if (data.length < OP_PUSHDATA1) { return opcode == data.length; - if (data.length < 256) + } + if (data.length < 256) { return opcode == OP_PUSHDATA1; - if (data.length < 65536) + } + if (data.length < 65536) { return opcode == OP_PUSHDATA2; + } // can never be used, but implemented for completeness return opcode == OP_PUSHDATA4; @@ -138,6 +146,57 @@ public void write(OutputStream stream) throws IOException { } } + public boolean isPositiveN() { + try { + decodePositiveN(); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public int decodePositiveN() { + if (isOpcodeSmallNumber()) { + return decodeOpN(); + } + + if (isPushData()) { + return decodePositiveNConsideringEncoding(); + } + + throw new IllegalArgumentException("Cannot decode positive number from chunk"); + } + + private boolean isOpcodeSmallNumber() { + return isOpCode() + && opcode >= ScriptOpCodes.OP_1 + && opcode <= ScriptOpCodes.OP_16; + } + + public boolean isOpCheckMultiSig() { + return isOpCode() && + (opcode == ScriptOpCodes.OP_CHECKMULTISIG || opcode == ScriptOpCodes.OP_CHECKMULTISIGVERIFY); + } + + private int decodePositiveNConsideringEncoding() { + if (isNull(data)) { + throw new IllegalArgumentException("Chunk has null data."); + } + int dataLength = data.length; + + int signByte = data[dataLength - 1] & 0x80; + boolean isPositive = signByte == 0; + if (!isPositive) { + throw new IllegalArgumentException("Number from chunk is not positive."); + } + + if (dataLength > 4) { + throw new IllegalArgumentException("Number from chunk has more than 4 bytes."); + } + BigInteger bigInteger = Utils.decodeMPI(Utils.reverseBytes(data), false); + return bigInteger.intValue(); // values up to Integer.MAX_VALUE can be cast as ints + } + @Override public String toString() { StringBuilder buf = new StringBuilder(); @@ -155,11 +214,17 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ScriptChunk other = (ScriptChunk) o; - return opcode == other.opcode && startLocationInProgram == other.startLocationInProgram - && Arrays.equals(data, other.data); + return opcode == other.opcode && + startLocationInProgram == other.startLocationInProgram && + Arrays.equals(data, other.data); } @Override diff --git a/src/main/java/co/rsk/bitcoinj/script/StandardRedeemScriptParser.java b/src/main/java/co/rsk/bitcoinj/script/StandardRedeemScriptParser.java index 599ff4404..ed7feec16 100644 --- a/src/main/java/co/rsk/bitcoinj/script/StandardRedeemScriptParser.java +++ b/src/main/java/co/rsk/bitcoinj/script/StandardRedeemScriptParser.java @@ -4,48 +4,46 @@ import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.bitcoinj.core.Sha256Hash; -import co.rsk.bitcoinj.core.Utils; import co.rsk.bitcoinj.crypto.TransactionSignature; import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.spongycastle.util.encoders.Hex; public class StandardRedeemScriptParser implements RedeemScriptParser { // In case of P2SH represents a scriptSig, where the last chunk is the redeem script (either standard or extended) - // Standard redeem script protected List redeemScriptChunks; - StandardRedeemScriptParser( - List redeemScriptChunks - ) { + StandardRedeemScriptParser(List redeemScriptChunks) { this.redeemScriptChunks = redeemScriptChunks; } @Override public int getM() { - checkArgument(redeemScriptChunks.get(0).isOpCode()); - return Script.decodeFromOpN(redeemScriptChunks.get(0).opcode); + ScriptChunk firstChunk = redeemScriptChunks.get(0); + return firstChunk.decodePositiveN(); } @Override public int findKeyInRedeem(BtcECKey key) { - checkArgument(redeemScriptChunks.get(0).isOpCode()); // P2SH scriptSig - int numKeys = Script.decodeFromOpN(redeemScriptChunks.get(redeemScriptChunks.size() - 2).opcode); + int numKeys = getN(); for (int i = 0; i < numKeys; i++) { if (Arrays.equals(redeemScriptChunks.get(1 + i).data, key.getPubKey())) { return i; } } - throw new IllegalStateException("Could not find matching key " + key.toString() + " in script " + this); + throw new IllegalStateException(String.format( + "Could not find matching key %s in script", key.getPublicKeyAsHex() + )); } @Override public List getPubKeys() { ArrayList result = Lists.newArrayList(); - int numKeys = Script.decodeFromOpN(redeemScriptChunks.get(redeemScriptChunks.size() - 2).opcode); + int numKeys = getN(); for (int i = 0; i < numKeys; i++) { result.add(BtcECKey.fromPublicOnly(redeemScriptChunks.get(1 + i).data)); } @@ -56,18 +54,16 @@ public List getPubKeys() { @Override public int findSigInRedeem(byte[] signatureBytes, Sha256Hash hash) { checkArgument(redeemScriptChunks.get(0).isOpCode()); // P2SH scriptSig - int numKeys = Script.decodeFromOpN(redeemScriptChunks.get(redeemScriptChunks.size() - 2).opcode); - TransactionSignature signature = TransactionSignature - .decodeFromBitcoin(signatureBytes, true); + int numKeys = getN(); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true); for (int i = 0; i < numKeys; i++) { if (BtcECKey.fromPublicOnly(redeemScriptChunks.get(i + 1).data).verify(hash, signature)) { return i; } } - throw new IllegalStateException( - "Could not find matching key for signature on " + hash.toString() + " sig " - + Utils.HEX.encode(signatureBytes) - ); + throw new IllegalStateException(String.format( + "Could not find matching key for signature %s on %s", Hex.toHexString(signatureBytes), hash + )); } @Override @@ -79,4 +75,9 @@ public List extractStandardRedeemScriptChunks() { public boolean hasErpFormat() { return false; } + + private int getN() { + ScriptChunk secondToLastChunk = redeemScriptChunks.get(redeemScriptChunks.size() - 2); // OP_N, last chunk is OP_CHECKMULTISIG + return secondToLastChunk.decodePositiveN(); + } } diff --git a/src/main/java/co/rsk/bitcoinj/wallet/Wallet.java b/src/main/java/co/rsk/bitcoinj/wallet/Wallet.java index 2d054402a..75c13ec8d 100644 --- a/src/main/java/co/rsk/bitcoinj/wallet/Wallet.java +++ b/src/main/java/co/rsk/bitcoinj/wallet/Wallet.java @@ -654,13 +654,27 @@ public void completeTx(SendRequest req) throws InsufficientMoneyException { if (req.shuffleOutputs) req.tx.shuffleOutputs(); - // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs. + // Now sign the legacy inputs, thus proving that we are entitled to redeem the connected outputs. if (req.signInputs) - signTransaction(req); + signLegacyTransaction(req); + + // Check virtual size. + int baseSize = req.tx.unsafeBitcoinSerialize().length; + int totalSize = baseSize; + // if the tx was signed, there's nothing else to consider + if (!req.signInputs) { + if (req.isSegwitCompatible) { + baseSize += calculateSegwitScriptSigSize(req.tx); + totalSize = baseSize; + totalSize += estimateBytesForSigning(bestCoinSelection); + } else { + baseSize += estimateBytesForSigning(bestCoinSelection); + totalSize = baseSize; + } + } - // Check size. - final int size = req.tx.unsafeBitcoinSerialize().length; - if (size > BtcTransaction.MAX_STANDARD_TX_SIZE) + int virtualSize = calculateVirtualSize(baseSize, totalSize); + if (virtualSize > BtcTransaction.MAX_STANDARD_TX_SIZE) throw new ExceededMaxTransactionSize(); // Label the transaction as being a user requested payment. This can be used to render GUI wallet @@ -676,12 +690,12 @@ public void completeTx(SendRequest req) throws InsufficientMoneyException { } /** - *

Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction + *

Given a send request containing a legacy transaction, attempts to sign it's inputs. This method expects transaction * to have all necessary inputs connected or they will be ignored.

*

Actual signing is done by pluggable {@link #signers} and it's not guaranteed that * transaction will be complete in the end.

*/ - public void signTransaction(SendRequest req) { + public void signLegacyTransaction(SendRequest req) { try { BtcTransaction tx = req.tx; List inputs = tx.getInputs(); @@ -731,7 +745,7 @@ private boolean adjustOutputDownwardsForFee( Coin feePerKb, boolean ensureMinRequiredFee ) { - final int size = calculateTxSize(tx, isSegwit, coinSelection); + final int size = calculateTxSizeForFees(tx, isSegwit, coinSelection); Coin fee = feePerKb.multiply(size).divide(1000); if (ensureMinRequiredFee && fee.compareTo(BtcTransaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) fee = BtcTransaction.REFERENCE_DEFAULT_MIN_TX_FEE; @@ -1007,7 +1021,7 @@ private FeeCalculation calculateFee( checkState(input.getScriptBytes().length == 0); } - int size = calculateTxSize(tx, req.isSegwitCompatible, selection); + int size = calculateTxSizeForFees(tx, req.isSegwitCompatible, selection); Coin feePerKb = req.feePerKb; if (needAtLeastReferenceFee && feePerKb.compareTo(BtcTransaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) { @@ -1026,32 +1040,53 @@ private FeeCalculation calculateFee( return result; } - private int calculateTxSize(BtcTransaction tx, boolean isSegwitCompatible, CoinSelection selection) { - int baseSize = calculateTxBaseSize(tx, isSegwitCompatible); - int totalSize = baseSize + estimateBytesForSigning(selection); - + /** + * Calculates the virtual size of a Bitcoin transaction for fee estimation purposes. + * When estimating fees, we assume the transaction is not yet signed, so we need to consider + * the expected size of the signatures. + * - If the transaction is legacy (non-SegWit), signature data is part of the base size, + * and the total size is equal to the base size. + * - If the transaction is SegWit-compatible, the scriptSig is a 36-byte fixed-size hash + * located in the input, so its part of the base size. Signatures are located in the + * witness, so they are just part of the total size. + * @param tx the unsigned Bitcoin transaction + * @param isSegwitCompatible whether the transaction is SegWit-compatible + * @param bestCoinSelection the selected UTXOs for the transaction + * @return the estimated virtual size of the transaction + */ + private int calculateTxSizeForFees(BtcTransaction tx, boolean isSegwitCompatible, CoinSelection bestCoinSelection) { + int baseSize = tx.unsafeBitcoinSerialize().length; + int totalSize; if (!isSegwitCompatible) { - return totalSize; + baseSize += estimateBytesForSigning(bestCoinSelection); + totalSize = baseSize; + } else { + baseSize += calculateSegwitScriptSigSize(tx); + totalSize = baseSize; + totalSize += estimateBytesForSigning(bestCoinSelection); } - - // As described in BIP141 - int txWeight = totalSize + (3 * baseSize); - return txWeight / 4; + return calculateVirtualSize(baseSize, totalSize); } - private static int calculateTxBaseSize(BtcTransaction tx, boolean isSegwit) { - int baseSize = tx.unsafeBitcoinSerialize().length; - if (!isSegwit) { - return baseSize; - } - - // at this time the script sig for every input is empty. - // in segwit-compatible, this is a 36-bytes-fixed-size hash, - // so we should count its bytes manually. + private int calculateSegwitScriptSigSize(BtcTransaction tx) { int segwitCompatibleScriptSigSize = 36; - baseSize += tx.getInputs().size() * segwitCompatibleScriptSigSize; + return tx.getInputs().size() * segwitCompatibleScriptSigSize; + } - return baseSize; + /** + * Calculates the virtual size of a Bitcoin transaction as defined in BIP141. + * The virtual size is a weighted size used to properly account for the discount SegWit + * provides on witness data. + * - For legacy transactions, {@code baseSize == totalSize}, so the virtual size equals both. + * - For SegWit transactions, {@code baseSize} excludes witness data, and {@code totalSize} includes it. + * + * @param baseSize the size of the transaction excluding witness data + * @param totalSize the full size of the transaction including witness data + * @return the virtual size in vbytes + */ + private int calculateVirtualSize(int baseSize, int totalSize) { + int txWeight = totalSize + (3 * baseSize); + return txWeight / 4; } private void addSuppliedInputs(BtcTransaction tx, List originalInputs) { diff --git a/src/test/java/co/rsk/bitcoinj/core/TransactionWitnessTest.java b/src/test/java/co/rsk/bitcoinj/core/TransactionWitnessTest.java index 44bfccd45..772b50ce0 100644 --- a/src/test/java/co/rsk/bitcoinj/core/TransactionWitnessTest.java +++ b/src/test/java/co/rsk/bitcoinj/core/TransactionWitnessTest.java @@ -1,37 +1,67 @@ package co.rsk.bitcoinj.core; +import co.rsk.bitcoinj.crypto.TransactionSignature; +import co.rsk.bitcoinj.params.MainNetParams; +import co.rsk.bitcoinj.script.RedeemScriptUtils; import co.rsk.bitcoinj.script.Script; import co.rsk.bitcoinj.script.ScriptBuilder; import co.rsk.bitcoinj.script.ScriptOpCodes; +import com.google.common.collect.Lists; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; -import org.spongycastle.util.encoders.Hex; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.*; public class TransactionWitnessTest { - private static final Script redeemScript = new Script( - Hex.decode("5221027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d35610072102d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856210346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e53ae") - ); // data from tx https://mempool.space/testnet/tx/1744459aeaf7369aadc9fc40de9ab2bf575b14e35029b35a7ee4bbd3de65af7f - private static final byte[] redeemScriptHash = Sha256Hash.hash(redeemScript.getProgram()); - + private static final int FIRST_INPUT_INDEX = 0; + private static final NetworkParameters MAINNET_PARAMS = MainNetParams.get(); + private static final List FEDERATION_KEYS = RedeemScriptUtils.getDefaultRedeemScriptKeys(); + private static final BtcECKey fedKey1 = FEDERATION_KEYS.get(0); + private static final BtcECKey fedKey2 = FEDERATION_KEYS.get(1); + private static final BtcECKey fedKey3 = FEDERATION_KEYS.get(2); + private static final List ERP_FEDERATION_KEYS = RedeemScriptUtils.getEmergencyRedeemScriptKeys(); + private static final long CSV_VALUE = 52_560L; + private static final Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + private static final Sha256Hash redeemScriptSerialized = Sha256Hash.of(redeemScript.getProgram()); + private static final Script p2shP2wshOutputScript = ScriptBuilder.createP2SHP2WSHOutputScript(redeemScript); + private static final int sigsPrefixCount = p2shP2wshOutputScript.getSigsPrefixCount(); private static final Script witnessScript = new ScriptBuilder() .number(ScriptOpCodes.OP_0) - .data(redeemScriptHash) + .data(redeemScriptSerialized.getBytes()) .build(); private static final byte[] witnessScriptHash = Utils.sha256hash160(witnessScript.getProgram()); - private static final byte[] op0 = new byte[] {}; - + private static final Coin fundingValue = Coin.FIFTY_COINS; + private static final BtcTransaction fundingTx = getFundingBtcTransaction(); private List pushes; + private BtcTransaction btcTx; + private Sha256Hash btcTxSigHashForWitness; + + + @Before + public void setUp() { + pushes = new ArrayList<>(); + btcTx = new BtcTransaction(MAINNET_PARAMS); + btcTx.addInput(fundingTx.getOutput(0)); + btcTx.addInput(fundingTx.getOutput(1)); + TransactionWitness witnessWithRedeemScript = createBaseWitnessThatSpendsFromErpRedeemScript(redeemScript); + btcTx.setWitness(FIRST_INPUT_INDEX, witnessWithRedeemScript); + btcTxSigHashForWitness = btcTx.hashForWitnessSignature(FIRST_INPUT_INDEX, redeemScript, fundingValue, + BtcTransaction.SigHash.ALL, false); + } @Test public void of_withValidPushes_createsTransactionWitnessWithPushes() { // arrange - pushes = new ArrayList<>(); - pushes.add(op0); pushes.add(witnessScriptHash); @@ -47,8 +77,6 @@ public void of_withValidPushes_createsTransactionWitnessWithPushes() { @Test public void of_withOneNullPush_throwsNPE() { // arrange - pushes = new ArrayList<>(); - pushes.add(op0); pushes.add(witnessScriptHash); @@ -163,6 +191,7 @@ public void equals_withTwoTransactionWitnessesWithTheSameElementsPushed_shouldBe TransactionWitness transactionWitness2 = new TransactionWitness(1); transactionWitness2.setPush(0, samePush); + // act int hashCode1 = transactionWitness1.hashCode(); int hashCode2 = transactionWitness2.hashCode(); @@ -186,6 +215,7 @@ public void equals_withTwoTransactionWitnessesWithOneDifferentPush_shouldBeFalse transactionWitness1.setPush(0, samePush); transactionWitness2.setPush(0, anotherDifferentPush); + // act int hashCode1 = transactionWitness1.hashCode(); int hashCode2 = transactionWitness2.hashCode(); @@ -193,4 +223,504 @@ public void equals_withTwoTransactionWitnessesWithOneDifferentPush_shouldBeFalse assertNotEquals(transactionWitness1, transactionWitness2); assertNotEquals(hashCode1, hashCode2); } + + @Test + public void getSigInsertionIndex_whenEmptyWitness_shouldThrownArrayIndexOutOfBoundsException() { + // arrange + Sha256Hash hashForSignature = Sha256Hash.of(new byte[]{1}); + pushes = new ArrayList<>(); + TransactionWitness transactionWitness = TransactionWitness.of(pushes); + + // act & assert + assertThrows(ArrayIndexOutOfBoundsException.class, () -> transactionWitness.getSigInsertionIndex(hashForSignature, fedKey1)); + } + + @Test + public void getSigInsertionIndex_whenMalformedRedeemScript_shouldThrowException() { + // arrange + Sha256Hash hashForSignature = Sha256Hash.of(new byte[]{1}); + Script customRedeemScript = new Script(new byte[2]); + byte[] emptyByte = {}; + pushes = new ArrayList<>(); + pushes.add(emptyByte); + pushes.add(customRedeemScript.getProgram()); + TransactionWitness transactionWitness = TransactionWitness.of(pushes); + + // act & assert + assertThrows(ScriptException.class, () -> transactionWitness.getSigInsertionIndex(hashForSignature, fedKey1)); + } + + @Test + public void getSigInsertionIndex_withWitnessWithoutSignatures_shouldReturnZeroForAllKeys() { + // arrange + TransactionWitness witness = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act + for (BtcECKey key: FEDERATION_KEYS) { + int sigInsertionIndex = witness.getSigInsertionIndex(btcTxSigHashForWitness, key); + + // assert + Assert.assertEquals(0, sigInsertionIndex); + } + } + + @Test + public void getSigInsertionIndex_withAGreaterPubKeySignatureInTheWitness_shouldReturnIndexCorrectly() { + // arrange + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int sigIndexForFedKey1BeforeSigning = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + int sigIndexForFedKey2BeforeSigning = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + Assert.assertEquals(0, sigIndexForFedKey1BeforeSigning); + Assert.assertEquals(0, sigIndexForFedKey2BeforeSigning); + + // sign with fedKey1 + signInput(btcTx, fedKey1, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithSignature = btcTx.getWitness(FIRST_INPUT_INDEX); + int sigIndexForFedKey1AfterSigning = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + int sigIndexForFedKey2AfterSigning = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + + Assert.assertEquals(1, sigIndexForFedKey1AfterSigning); + Assert.assertEquals(1, sigIndexForFedKey2AfterSigning); + } + + @Test + public void getSigInsertionIndex_withALowerPubKeySignatureInTheWitness_shouldReturnIndexCorrectly() { + // arrange + // sign with fedKey2 + signInput(btcTx, fedKey2, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithSignature = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act + int sigIndexForFedKey1 = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + + // assert + Assert.assertEquals(0, sigIndexForFedKey1); + } + + @Test + public void getSigInsertionIndex_withTheSamePubKeyWhichSignatureAlreadyInTheWitness_shouldReturnIndexCorrectly() { + // arrange + // sign with fedKey1 + signInput(btcTx, fedKey1, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithSignature = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act + int sigIndexAfterInsertingSignature = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + + // assert + assertEquals(1, sigIndexAfterInsertingSignature); + } + + @Test + public void getSigInsertionIndex_withDifferentSignaturesInTheWitness_shouldReturnIndexCorrectly() { + // arrange + // sign with fedKey2 + signInput(btcTx, fedKey2, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithSignature = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int sigIndexForFedKey1 = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + int sigIndexForFedKey3 = witnessWithSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + + Assert.assertEquals(0, sigIndexForFedKey1); + // fedKey3 should be inserted after fedKey2 + Assert.assertEquals(1, sigIndexForFedKey3); + + // now fedKey1 signs the input and pushes fedKey2's signature one position + signInput(btcTx, fedKey1, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithFedKey1Signature = btcTx.getWitness(FIRST_INPUT_INDEX); + + sigIndexForFedKey3 = witnessWithFedKey1Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + Assert.assertEquals(2, sigIndexForFedKey3); + } + + @Test + public void getSigInsertionIndex_withFedKey3SignatureInTheWitness_shouldReturnIndexCorrectly() { + // arrange + // sign with fedKey3 + signInput(btcTx, fedKey3, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + TransactionWitness witnessWithFedKey3Signature = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act + int sigIndexForFedKey1 = witnessWithFedKey3Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + int sigIndexForFedKey2 = witnessWithFedKey3Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + + // assert + Assert.assertEquals(0, sigIndexForFedKey1); + Assert.assertEquals(0, sigIndexForFedKey2); + } + + @Test + public void getSigInsertionIndex_withWitnessFilledWithSignatures_shouldReturnTheProperIndex() { + // arrange + int i = 0; + while(i < redeemScript.getNumberOfSignaturesRequiredToSpend()) { + BtcECKey key = FEDERATION_KEYS.get(i); + signInput(btcTx, key, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + i++; + } + + TransactionWitness signedTransactionWitness = btcTx.getWitness(FIRST_INPUT_INDEX); + BtcECKey key = FEDERATION_KEYS.get(i); + + // act + int sigInsertionIndex = signedTransactionWitness.getSigInsertionIndex(btcTxSigHashForWitness, key); + + //assert + assertEquals(i, sigInsertionIndex); + } + + @Test + public void updateWitnessWithSignature_withOneSignature_shouldReturnAWitnessWithTheSignaturePlacedCorrectly() { + // signing order: [fedKey1] + // arrange + byte[] signatureEncodeToBitcoin = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + + TransactionWitness witnessWithoutSignature = btcTx.getWitness(FIRST_INPUT_INDEX); + int sigIndex = witnessWithoutSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, sigIndex); + + // act + TransactionWitness witnessWithOneSignature = witnessWithoutSignature.updateWitnessWithSignature( + p2shP2wshOutputScript, signatureEncodeToBitcoin, sigIndex); + + // assert + assertSignaturesAreInOrder(witnessWithOneSignature, Lists.newArrayList(signatureEncodeToBitcoin)); + } + + @Test + public void updateWitnessWithSignature_twoTimesWithTheSameSignature_shouldInsertBoth() { + // signing order: [fedKey1, fedKey1] + // expected signatures order: [signatureFed1, signatureFed1] + // arrange + byte[] fedKey1TxSignature = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int sigIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, sigIndex); + TransactionWitness witnessWithOneSignature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1TxSignature, sigIndex); + assertSignaturesAreInOrder(witnessWithOneSignature, Lists.newArrayList(fedKey1TxSignature)); + + int sigIndexAfterSigning = witnessWithOneSignature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertNotEquals(sigIndex, sigIndexAfterSigning); + assertEquals(1, sigIndexAfterSigning); + + TransactionWitness witnessWithTwoSignatures = witnessWithOneSignature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1TxSignature, sigIndexAfterSigning); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1TxSignature, fedKey1TxSignature)); + } + + @Test + public void updateWitnessWithSignature_withWitnessFilledWithSignatures_shouldThrowAnError() { + // signing order: [fedKey1, .., maxFedKey, fedKey1] signatures + // expected signatures order: [signatureFed1, .., signatureMaxFed] + // arrange + int i = 0; + int numberOfSignaturesRequiredToSpend = redeemScript.getNumberOfSignaturesRequiredToSpend(); + while(i < numberOfSignaturesRequiredToSpend) { + BtcECKey key = FEDERATION_KEYS.get(i); + signInput(btcTx, key, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + i++; + } + + // getSuffixCount doesn't consider OP_NOTIF param op code, so the calculation for + // the number of signatures required in updateWitnessWithSignature is + // wrong. + BtcECKey key = FEDERATION_KEYS.get(i++); + signInput(btcTx, key, FIRST_INPUT_INDEX, btcTxSigHashForWitness); + + byte[] txSig = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + TransactionWitness signedTransactionWitness = btcTx.getWitness(FIRST_INPUT_INDEX); + key = FEDERATION_KEYS.get(i); + int sigInsertionIndex = signedTransactionWitness.getSigInsertionIndex(btcTxSigHashForWitness, key); + + // act & assert + assertTrue(numberOfSignaturesRequiredToSpend < sigInsertionIndex); + assertEquals(numberOfSignaturesRequiredToSpend + 1, sigInsertionIndex); + + // It fails because the witness is already filled with signatures. + // Then, the sigIndex is higher than the amount of signatures required. + assertThrows(IllegalArgumentException.class, () -> signedTransactionWitness.updateWitnessWithSignature( + p2shP2wshOutputScript, txSig, sigInsertionIndex)); + } + + @Test + public void updateWitnessWithSignature_withTheLowestSignatureInWitness_shouldInsertTheNewOneAsSecond() { + // signing order: [fedKey1, fedKey2] + // expected signatures order: [signatureFed1, signatureFed2] + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + int fed1SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + + // act & assert + TransactionWitness witnessWithFedKey1Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey1Signature, Lists.newArrayList(fedKey1SignatureEncoded)); + + byte[] fedKey2TxSignature = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + int fed2SigInsertionIndex = witnessWithFedKey1Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(1, fed2SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey1Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2TxSignature, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1SignatureEncoded, fedKey1SignatureEncoded)); + } + + @Test + public void updateWitnessWithSignature_withALowerSignatureInWitness_shouldInsertTheNewOneAsFirst() { + // signing order: [fedKey2, fedKey1] + // expected signatures order: [signatureFed1, signatureFed2] + // arrange + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int fed2SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(0, fed2SigInsertionIndex); + TransactionWitness witnessWithFedKey2Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + assertSignaturesAreInOrder(witnessWithFedKey2Signature, Lists.newArrayList(fedKey2SignatureEncoded)); + + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + int fed1SigInsertionIndex = witnessWithFedKey2Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey2Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + ArrayList expectedSignatures = Lists.newArrayList(fedKey1SignatureEncoded, fedKey2SignatureEncoded); + assertSignaturesAreInOrder(witnessWithTwoSignatures, expectedSignatures); + } + + @Test + public void updateWitnessWithSignature_withThreeSignaturesInDescendingOrder_shouldBeInsertedRespectingTheOrder() { + // signing order: [fedKey3, fedKey2, fedKey1] + // expected signatures order: [signatureFed1, signatureFed2, signatureFed3] + // arrange + byte[] fedKey3SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey3, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int fed3SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + assertEquals(0, fed3SigInsertionIndex); + TransactionWitness witnessWithFedKey3Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey3SignatureEncoded, fed3SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey3Signature, Lists.newArrayList(fedKey3SignatureEncoded)); + + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + int fed2SigInsertionIndex = witnessWithFedKey3Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(0, fed2SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey3Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + int fed1SigInsertionIndex = witnessWithTwoSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + TransactionWitness witnessWithThreeSignatures = witnessWithTwoSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithThreeSignatures, Lists.newArrayList(fedKey1SignatureEncoded, fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + } + + @Test + public void updateWitnessWithSignature_withThreeUnorderedSignatures_shouldBeInsertedRespectingTheOrder() { + // signing order: [fedKey1, fedKey3, fedKey2] + // expected signatures order: [signatureFed1, signatureFed2, signatureFed3] + // arrange + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + + // act & assert + int fed1SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + TransactionWitness witnessWithFedKey1Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey1Signature, Lists.newArrayList(fedKey1SignatureEncoded)); + + byte[] fedKey3SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey3, btcTxSigHashForWitness); + int fed3SigInsertionIndex = witnessWithFedKey1Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + assertEquals(1, fed3SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey1Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey3SignatureEncoded, fed3SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey3SignatureEncoded)); + + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + int fed2SigInsertionIndex = witnessWithTwoSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(1, fed2SigInsertionIndex); + TransactionWitness witnessWithThreeSignatures = witnessWithTwoSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithThreeSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + } + + @Test + public void updateWitnessWithSignature_withThreeSignaturesInAscendingOrder_shouldBeInsertedRespectingTheOrder() { + // signing order: [fedKey1, fedKey2, fedKey3] + // expected signatures order: [signatureFed1, signatureFed2, signatureFed3] + // arrange + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + int fed1SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + // act & assert + TransactionWitness witnessWithFedKey1Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey1Signature, Lists.newArrayList(fedKey1SignatureEncoded)); + + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + int fed2SigInsertionIndex = witnessWithFedKey1Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(1, fed2SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey1Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded)); + + byte[] fedKey3SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey3, btcTxSigHashForWitness); + int fed3SigInsertionIndex = witnessWithTwoSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + assertEquals(2, fed3SigInsertionIndex); + TransactionWitness witnessWithThreeSignatures = witnessWithTwoSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey3SignatureEncoded, fed3SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithThreeSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + } + + @Test + public void updateWitnessWithSignature_withThreeSignaturesInADifferentOrder_shouldBeInsertedRespectingTheOrder() { + // signing order: [fedKey2, fedKey1, fedKey3] + // expected signatures order: [signatureFed1, signatureFed2, signatureFed3] + // arrange + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + int fed2SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(0, fed2SigInsertionIndex); + // act & assert + TransactionWitness witnessWithFedKey2Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey2Signature, Lists.newArrayList(fedKey2SignatureEncoded)); + + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + int fed1SigInsertionIndex = witnessWithFedKey2Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey2Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + assertEquals(0, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded)); + + byte[] fedKey3SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey3, btcTxSigHashForWitness); + int fed3SigInsertionIndex = witnessWithTwoSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + assertEquals(2, fed3SigInsertionIndex); + TransactionWitness witnessWithThreeSignatures = witnessWithTwoSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey3SignatureEncoded, fed3SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithThreeSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + } + + @Test + public void updateWitnessWithSignature_withThreeSignaturesInASecondDifferentOrder_shouldBeInsertedRespectingTheOrder() { + // signing order: [fedKey3, fedKey1, fedKey2] + // expected signatures order: [signatureFed1, signatureFed2, signatureFed3] + // arrange + byte[] fedKey3SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey3, btcTxSigHashForWitness); + TransactionWitness witnessWithoutSignatures = btcTx.getWitness(FIRST_INPUT_INDEX); + int fed3SigInsertionIndex = witnessWithoutSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey3); + assertEquals(0, fed3SigInsertionIndex); + // act & assert + TransactionWitness witnessWithFedKey3Signature = witnessWithoutSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey3SignatureEncoded, fed3SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithFedKey3Signature, Lists.newArrayList(fedKey3SignatureEncoded)); + + byte[] fedKey1SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey1, btcTxSigHashForWitness); + int fed1SigInsertionIndex = witnessWithFedKey3Signature.getSigInsertionIndex(btcTxSigHashForWitness, fedKey1); + assertEquals(0, fed1SigInsertionIndex); + TransactionWitness witnessWithTwoSignatures = witnessWithFedKey3Signature.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey1SignatureEncoded, fed1SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithTwoSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey3SignatureEncoded)); + + byte[] fedKey2SignatureEncoded = getTransactionSignatureEncodedToBtc(fedKey2, btcTxSigHashForWitness); + int fed2SigInsertionIndex = witnessWithTwoSignatures.getSigInsertionIndex(btcTxSigHashForWitness, fedKey2); + assertEquals(1, fed2SigInsertionIndex); + TransactionWitness witnessWithThreeSignatures = witnessWithTwoSignatures.updateWitnessWithSignature( + p2shP2wshOutputScript, fedKey2SignatureEncoded, fed2SigInsertionIndex); + + assertSignaturesAreInOrder(witnessWithThreeSignatures, Lists.newArrayList(fedKey1SignatureEncoded, + fedKey2SignatureEncoded, fedKey3SignatureEncoded)); + } + + private void assertSignaturesAreInOrder(TransactionWitness witness, List expectedSignatures) { + int index = 0; + for (byte[] expectedSignature : expectedSignatures) { + int signaturePosition = sigsPrefixCount + index; + byte[] actualSignature = witness.getPush(signaturePosition); + assertArrayEquals(expectedSignature, actualSignature); + index++; + } + } + + private byte[] getTransactionSignatureEncodedToBtc(BtcECKey key, Sha256Hash sigHash) { + byte[] federatorSig = key.sign(sigHash).encodeToDER(); + BtcECKey.ECDSASignature signature = BtcECKey.ECDSASignature.decodeFromDER(federatorSig); + TransactionSignature txSig = new TransactionSignature(signature, BtcTransaction.SigHash.ALL, false); + return txSig.encodeToBitcoin(); + } + + private static BtcTransaction getFundingBtcTransaction() { + BtcTransaction btcTx = new BtcTransaction(MAINNET_PARAMS); + final Address userAddress = BtcECKey.fromPrivate(BigInteger.valueOf(901)).toAddress(MAINNET_PARAMS); + btcTx.addOutput(fundingValue, userAddress); + btcTx.addOutput(fundingValue, userAddress); + return btcTx; + } + + private void signInput(BtcTransaction btcTx, BtcECKey key, int inputIndex, Sha256Hash sigHash) { + byte[] txSigEncodedForBitcoin = getTransactionSignatureEncodedToBtc(key, sigHash); + + TransactionWitness transactionWitness = btcTx.getWitness(inputIndex); + int sigIndex = transactionWitness.getSigInsertionIndex(sigHash, key); + TransactionWitness witnessWithSignature = transactionWitness.updateWitnessWithSignature(p2shP2wshOutputScript, + txSigEncodedForBitcoin, sigIndex); + btcTx.setWitness(inputIndex, witnessWithSignature); + } + + private static TransactionWitness createBaseWitnessThatSpendsFromErpRedeemScript(Script redeemScript) { + int pushForEmptyByte = 1; + int pushForOpNotif = 1; + int pushForRedeemScript = 1; + int numberOfSignaturesRequiredToSpend = redeemScript.getNumberOfSignaturesRequiredToSpend(); + int witnessSize = pushForRedeemScript + pushForOpNotif + numberOfSignaturesRequiredToSpend + pushForEmptyByte; + + List pushes = new ArrayList<>(witnessSize); + byte[] emptyByte = {}; + pushes.add(emptyByte); // OP_0 + + for (int i = 0; i < numberOfSignaturesRequiredToSpend; i++) { + pushes.add(emptyByte); + } + + byte[] opNotIf = {}; + pushes.add(opNotIf); + pushes.add(redeemScript.getProgram()); + return TransactionWitness.of(pushes); + } } diff --git a/src/test/java/co/rsk/bitcoinj/script/FlyoverRedeemScriptParserTest.java b/src/test/java/co/rsk/bitcoinj/script/FlyoverRedeemScriptParserTest.java index 0bd23a5fa..843331d96 100644 --- a/src/test/java/co/rsk/bitcoinj/script/FlyoverRedeemScriptParserTest.java +++ b/src/test/java/co/rsk/bitcoinj/script/FlyoverRedeemScriptParserTest.java @@ -78,14 +78,14 @@ public void getM_whenFlyoverRedeemScriptContainsP2shErpRedeemScript_shouldReturn private void assertGetMValue(Script flyoverRedeemScript) { // Arrange - final int EXPECTED_M = 5; + int expectedNumberOfSignatures = keys.size() / 2 + 1; FlyoverRedeemScriptParser flyoverRedeemScriptParser = new FlyoverRedeemScriptParser(flyoverRedeemScript.getChunks()); // Act - int actualM = flyoverRedeemScriptParser.getM(); + int actualNumberOfSignatures = flyoverRedeemScriptParser.getM(); // Assert - assertEquals(EXPECTED_M, actualM); + assertEquals(expectedNumberOfSignatures, actualNumberOfSignatures); } @Test @@ -129,7 +129,7 @@ public void findKeyInRedeem_whenKeyIsNotInP2shErpRedeemScript_shouldThrowIllegal private void assertThrowsIllegalStateException(Script flyoverRedeemScript) { // Arrange - final BtcECKey differentKey = BtcECKey.fromPrivate(BigInteger.valueOf(1000)); + final BtcECKey differentKey = BtcECKey.fromPrivate(BigInteger.valueOf(10001)); FlyoverRedeemScriptParser flyoverRedeemScriptParser = new FlyoverRedeemScriptParser(flyoverRedeemScript.getChunks()); // Act - Assert diff --git a/src/test/java/co/rsk/bitcoinj/script/RedeemScriptUtils.java b/src/test/java/co/rsk/bitcoinj/script/RedeemScriptUtils.java index 39868d5b0..0d1524a75 100644 --- a/src/test/java/co/rsk/bitcoinj/script/RedeemScriptUtils.java +++ b/src/test/java/co/rsk/bitcoinj/script/RedeemScriptUtils.java @@ -5,6 +5,7 @@ import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.bitcoinj.core.Utils; import java.math.BigInteger; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -102,22 +103,23 @@ public static Script createFlyoverRedeemScript(byte[] derivationArgumentsHashByt } public static List getDefaultRedeemScriptKeys() { - List keys = Arrays.asList( - BtcECKey.fromPrivate(BigInteger.valueOf(100)), - BtcECKey.fromPrivate(BigInteger.valueOf(200)), - BtcECKey.fromPrivate(BigInteger.valueOf(300)), - BtcECKey.fromPrivate(BigInteger.valueOf(400)), - BtcECKey.fromPrivate(BigInteger.valueOf(500)), - BtcECKey.fromPrivate(BigInteger.valueOf(600)), - BtcECKey.fromPrivate(BigInteger.valueOf(700)), - BtcECKey.fromPrivate(BigInteger.valueOf(800)), - BtcECKey.fromPrivate(BigInteger.valueOf(900)) - ); + List keys = getNKeys(20); keys.sort(BtcECKey.PUBKEY_COMPARATOR); return keys; } + public static List getNKeys(int n) { + ArrayList keys = new ArrayList<>(n); + for (int i = 1; i <= n; i++) { + long seed = i * 100; + BtcECKey btcECKey = BtcECKey.fromPrivate(BigInteger.valueOf(seed)); + keys.add(btcECKey); + } + + return keys; + } + public static List getEmergencyRedeemScriptKeys() { List keys = Arrays.asList( BtcECKey.fromPrivate(BigInteger.valueOf(101)), diff --git a/src/test/java/co/rsk/bitcoinj/script/RedeemScriptValidatorTest.java b/src/test/java/co/rsk/bitcoinj/script/RedeemScriptValidatorTest.java index fa0363fc6..30d5b89af 100644 --- a/src/test/java/co/rsk/bitcoinj/script/RedeemScriptValidatorTest.java +++ b/src/test/java/co/rsk/bitcoinj/script/RedeemScriptValidatorTest.java @@ -23,10 +23,11 @@ public class RedeemScriptValidatorTest { private final BtcECKey ecKey1 = BtcECKey.fromPrivate(BigInteger.valueOf(110)); private final BtcECKey ecKey2 = BtcECKey.fromPrivate(BigInteger.valueOf(220)); private final BtcECKey ecKey3 = BtcECKey.fromPrivate(BigInteger.valueOf(330)); + private final BtcECKey ecKey4 = BtcECKey.fromPrivate(BigInteger.valueOf(440)); private Script standardRedeemScript; private Script flyoverStandardRedeemScript; - private Script nonstandardErpRedeemScript; + private Script nonStandardErpRedeemScript; private Script flyoverNonStandardErpRedeemScript; private Script p2shErpRedeemScript; private Script flyoverP2shErpRedeemScript; @@ -42,11 +43,11 @@ public void setUp() { flyoverStandardRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( FLYOVER_DERIVATION_HASH.getBytes(), standardRedeemScript); - nonstandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( + nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( defaultRedeemScriptKeys, emergencyRedeemScriptKeys, CSV_VALUE); flyoverNonStandardErpRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( FLYOVER_DERIVATION_HASH.getBytes(), - nonstandardErpRedeemScript); + nonStandardErpRedeemScript); p2shErpRedeemScript = RedeemScriptUtils.createP2shErpRedeemScript(defaultRedeemScriptKeys, emergencyRedeemScriptKeys, CSV_VALUE); @@ -59,8 +60,8 @@ public void setUp() { @Test public void isRedeemLikeScript_whenStandardMultisig_shouldReturnTrue() { - List chunks = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys).getChunks(); - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(chunks)); + List standardRedeemScriptChunks = standardRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(standardRedeemScriptChunks)); } @Test @@ -73,65 +74,37 @@ public void isRedeemLikeScript_whenNonStandardErpRedeemScriptParserHardcoded_sho @Test public void isRedeemLikeScript_whenNonStandardErpRedeemScriptParser_shouldReturnTrue() { - List nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE).getChunks(); - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(nonStandardErpRedeemScript)); + List nonStandardErpRedeemScriptChunks = nonStandardErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(nonStandardErpRedeemScriptChunks)); } @Test public void isRedeemLikeScript_whenP2shErpRedeemScriptParserHardcoded_shouldReturnTrue() { - List p2shRedeemScriptChunks = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE).getChunks(); - - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(p2shRedeemScriptChunks)); + List p2shErpRedeemScriptChunks = p2shErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(p2shErpRedeemScriptChunks)); } @Test public void isRedeemLikeScript_whenFlyoverStandardMultisigRedeemScript_shouldReturnTrue() { - Script standardRedeemScript = RedeemScriptUtils.createStandardRedeemScript( - defaultRedeemScriptKeys); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - standardRedeemScript - ).getChunks(); - - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(flyoverRedeemScriptChunks)); + List flyoverStandardRedeemScriptChunks = flyoverStandardRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(flyoverStandardRedeemScriptChunks)); } @Test public void isRedeemLikeScript_whenFlyoverNonStandardErpRedeemScript_shouldReturnTrue() { - Script nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - nonStandardErpRedeemScript - ).getChunks(); - - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(flyoverRedeemScriptChunks)); + List flyoverNonStandardErpRedeemScriptChunks = flyoverNonStandardErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(flyoverNonStandardErpRedeemScriptChunks)); } @Test public void isRedeemLikeScript_whenFlyoverP2shErpRedeemScript_shouldReturnTrue() { - Script p2shErpRedeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE); - List p2shRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - p2shErpRedeemScript - ).getChunks(); - - Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(p2shRedeemScriptChunks)); + List flyoverP2shErpRedeemScriptChunks = flyoverP2shErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.isRedeemLikeScript(flyoverP2shErpRedeemScriptChunks)); } @Test public void isRedeemLikeScript_invalid_redeem_script_missing_checkSig() { - List chunksWithoutCheckSig = RedeemScriptValidator.removeOpCheckMultisig( - RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys) - ); - + List chunksWithoutCheckSig = RedeemScriptValidator.removeOpCheckMultisig(standardRedeemScript); Assert.assertFalse(RedeemScriptValidator.isRedeemLikeScript(chunksWithoutCheckSig)); } @@ -144,24 +117,95 @@ public void isRedeemLikeScript_invalid_redeem_script_insufficient_chunks() { .data(ecKey3.getPubKey()) .build(); - Assert.assertFalse(RedeemScriptValidator.isRedeemLikeScript(redeemScript.getChunks())); + List redeemScriptChunks = redeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.isRedeemLikeScript(redeemScriptChunks)); } @Test public void hasStandardRedeemScriptStructure_standard_redeem_script() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - Assert.assertTrue(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScript.getChunks())); + List standardRedeemScriptChunks = standardRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasStandardRedeemScriptStructure(standardRedeemScriptChunks)); } @Test - public void hasStandardRedeemScriptStructure_non_standard_redeem_script() { - Script redeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); + public void hasStandardRedeemScriptStructure_with21defaultRedeemScriptKeys_shoulThrowIAE() { + List bigNumberOfDefaultRedeemScriptKeys = RedeemScriptUtils.getNKeys(21); + Assert.assertThrows(IllegalArgumentException.class, () -> RedeemScriptUtils.createStandardRedeemScript(bigNumberOfDefaultRedeemScriptKeys)); + } - Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScript.getChunks())); + @Test + public void hasStandardRedeemScriptStructure_withLessKeysPushedThanExpected_shouldBeFalse() { + ScriptBuilder builder = new ScriptBuilder(); + Script redeemScript = builder + .op(ScriptOpCodes.OP_4) + .data(ecKey1.getPubKey()) + .op(ScriptOpCodes.OP_4) + .op(ScriptOpCodes.OP_CHECKMULTISIG) + .build(); + List redeemScriptChunks = redeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScriptChunks)); + } + + @Test + public void hasStandardRedeemScriptStructure_withNegativeThreshold_shouldBeFalse() { + for (int n=0; n<20; n++) { + int i = -(1 << n); // i = -(2^n) + ScriptBuilder builder = new ScriptBuilder(); + Script redeemScript = builder + .number(i) + .data(ecKey1.getPubKey()) + .data(ecKey2.getPubKey()) + .data(ecKey3.getPubKey()) + .data(ecKey4.getPubKey()) + .op(ScriptOpCodes.OP_4) + .op(ScriptOpCodes.OP_CHECKMULTISIG) + .build(); + + List redeemScriptChunks = redeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScriptChunks)); + } + } + + @Test + public void hasStandardRedeemScriptStructure_withNegativeTotalKeys_shouldBeFalse() { + for (int n=0; n<20; n++) { + int i = -(1 << n); // i = -(2^n) + ScriptBuilder builder = new ScriptBuilder(); + Script redeemScript = builder + .op(ScriptOpCodes.OP_4) + .data(ecKey1.getPubKey()) + .data(ecKey2.getPubKey()) + .data(ecKey3.getPubKey()) + .data(ecKey4.getPubKey()) + .number(i) + .op(ScriptOpCodes.OP_CHECKMULTISIG) + .build(); + + List redeemScriptChunks = redeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScriptChunks)); + } + } + + @Test + public void hasStandardRedeemScriptStructure_withARedeemLikeScript_withoutOpCheckMultisigAsLastOpcode_shouldBeFalse() { + ScriptBuilder builder = new ScriptBuilder(); + Script redeemScript = builder + .op(ScriptOpCodes.OP_2) + .data(ecKey1.getPubKey()) + .data(ecKey2.getPubKey()) + .data(ecKey3.getPubKey()) + .op(ScriptOpCodes.OP_CHECKMULTISIG) + .op(ScriptOpCodes.OP_ENDIF) + .build(); + + List redeemScriptChunks = redeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(redeemScriptChunks)); + } + + @Test + public void hasStandardRedeemScriptStructure_non_standard_redeem_script() { + List nonStandardErpRedeemScriptChunks = nonStandardErpRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasStandardRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -170,22 +214,26 @@ public void hasNonStandardErpRedeemScriptStructure_whenCustomRedeemScript_should defaultRedeemScriptKeys ); - Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(customRedeemScript.getChunks())); + List customRedeemScriptChunks = customRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(customRedeemScriptChunks)); } @Test public void hasNonStandardErpRedeemScriptStructure_whenStandardRedeemScript_shouldReturnFalse() { - Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(standardRedeemScript.getChunks())); + List standardRedeemScriptChunks = standardRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(standardRedeemScriptChunks)); } @Test public void hasNonStandardErpRedeemScriptStructure_whenP2shErpRedeemScript_shouldReturnFalse() { - Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(p2shErpRedeemScript.getChunks())); + List p2shErpRedeemScriptChunks = p2shErpRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(p2shErpRedeemScriptChunks)); } @Test public void hasNonStandardErpRedeemScriptStructure_whenFlyoverStandardRedeemScript_shouldReturnFalse() { - Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(flyoverStandardRedeemScript.getChunks())); + List flyoverStandardRedeemScriptChunks = flyoverStandardRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(flyoverStandardRedeemScriptChunks)); } @Test @@ -196,18 +244,14 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 10L ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemsScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemsScriptChunks)); } @Test public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemScriptTwoBytesCsvValue_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = nonStandardErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -218,7 +262,8 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 130L // Any value above 127 needs an extra byte to indicate the sign ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -229,7 +274,8 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 100_000L ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -240,7 +286,8 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 33_000L // Any value above 32_767 needs an extra byte to indicate the sign ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -251,7 +298,8 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 10_000_000L ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test @@ -262,25 +310,15 @@ public void hasNonStandardErpRedeemScriptStructure_whenNonStandardErpFedRedeemSc 8_400_000L // Any value above 8_388_607 needs an extra byte to indicate the sign ); - Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(redeemScript.getChunks())); + List nonStandardErpRedeemScriptChunks = redeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure(nonStandardErpRedeemScriptChunks)); } @Test public void hasNonStandardErpRedeemScriptStructure_whenFlyoverNonStandardErpRedeemScriptRemovingPrefix_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); - // Remove fast bridge prefix - List chunks = flyoverRedeemScript.getChunks(); - List chunksWithoutFlyoverPrefix = chunks.subList(2, chunks.size()); + List flyoverNonStandardErpRedeemScriptChunks = flyoverNonStandardErpRedeemScript.getChunks(); + List chunksWithoutFlyoverPrefix = flyoverNonStandardErpRedeemScriptChunks.subList(2, flyoverNonStandardErpRedeemScriptChunks.size()); Assert.assertTrue(RedeemScriptValidator.hasNonStandardErpRedeemScriptStructure( chunksWithoutFlyoverPrefix) @@ -289,69 +327,43 @@ public void hasNonStandardErpRedeemScriptStructure_whenFlyoverNonStandardErpRede @Test public void hasFlyoverPrefix_whenEmptyFlyoverPrefix_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - byte[] emptyFlyoverPrefix = {}; Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( emptyFlyoverPrefix, - redeemScript + standardRedeemScript ); - Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverRedeemScriptChunks = flyoverRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenStandardMultisigRedeemScript_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(redeemScript.getChunks())); + List standardRedeemScriptChunks = standardRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(standardRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenP2shErpRedeemScript_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(redeemScript.getChunks())); + List p2shErpRedeemScriptChunks = p2shErpRedeemScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(p2shErpRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenScriptSig_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverP2shErpRedeemScript); + Script scriptSig = p2shOutputScript.createEmptyInputScript(null, flyoverP2shErpRedeemScript); - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); - - Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); - - Script scriptSig = p2shOutputScript.createEmptyInputScript(null, flyoverRedeemScript); - - Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(scriptSig.getChunks())); + List scriptSigChunks = scriptSig.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(scriptSigChunks)); } @Test public void hasFlyoverPrefix_whenP2shOutputScript_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverP2shErpRedeemScript); - Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); - - Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(p2shOutputScript.getChunks())); + List p2shOutputScriptChunks = p2shOutputScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverPrefix(p2shOutputScriptChunks)); } @Test @@ -363,7 +375,8 @@ public void hasFlyoverPrefix_whenFlyoverPrefixAndInvalidScript_shouldReturnTrue( invalidScript ); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverRedeemScriptChunks = flyoverRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScriptChunks)); } @Test @@ -375,62 +388,37 @@ public void hasFlyoverPrefix_whenZeroFlyoverPrefixAndInvalidScript_shouldReturnT invalidScript ); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverRedeemScriptChunks = flyoverRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenFlyoverNonStandardErpRedeemScriptParserHardcoded_shouldReturnTrue() { - Script redeemScript = new Script(NON_STANDARD_ERP_TESTNET_REDEEM_SCRIPT_SERIALIZED); - - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + Script flyoverNonStandardErpTestnetRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript + nonStandardErpTestnetRedeemScript ); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverNonStandardErpTestnetRedeemScriptChunks = flyoverNonStandardErpTestnetRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverNonStandardErpTestnetRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenFlyoverStandardMultisigRedeemScript_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); - - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverStandardRedeemScriptChunks = flyoverStandardRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverStandardRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenFlyoverNonStandardErpRedeemScript_shouldReturnTrue() { - Script nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - nonStandardErpRedeemScript - ); - - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverNonStandardErpRedeemScriptChunks = flyoverNonStandardErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverNonStandardErpRedeemScriptChunks)); } @Test public void hasFlyoverPrefix_whenFlyoverP2shErpRedeemScript_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, - CSV_VALUE - ); - - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); - - Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverRedeemScript.getChunks())); + List flyoverP2shErpRedeemScriptChunks = flyoverP2shErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverPrefix(flyoverP2shErpRedeemScriptChunks)); } @Test @@ -446,125 +434,82 @@ public void hasFlyoverRedeemScriptStructure_whenFlyoverNoRedeemScript_shouldRetu @Test public void hasFlyoverRedeemScriptStructure_whenEmptyFlyoverDerivationHash_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( new byte[]{}, - redeemScript + standardRedeemScript ).getChunks(); Assert.assertFalse(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenScriptSig_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverStandardRedeemScript); + Script scriptSig = p2shOutputScript.createEmptyInputScript(null, flyoverStandardRedeemScript); - Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); - Script scriptSig = p2shOutputScript.createEmptyInputScript(null, flyoverRedeemScript); - - Assert.assertFalse(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(scriptSig.getChunks())); + List scriptSigChunks = scriptSig.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(scriptSigChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenP2shOutputScript_shouldReturnFalse() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ); - - Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); - - Assert.assertFalse(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(p2shOutputScript.getChunks())); + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverStandardRedeemScript); + List p2shOutputScriptChunks = p2shOutputScript.getChunks(); + Assert.assertFalse(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(p2shOutputScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenZeroFlyoverDerivationHash_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( Sha256Hash.ZERO_HASH.getBytes(), - redeemScript + standardRedeemScript ).getChunks(); Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenFlyoverStandardMultisigRedeemScript_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ).getChunks(); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); + List flyoverStandardRedeemScriptChunks = flyoverStandardRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverStandardRedeemScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenFlyoverNonStandardErpRedeemScript_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ).getChunks(); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); + List flyoverNonStandardErpRedeemScriptChunks = flyoverNonStandardErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverNonStandardErpRedeemScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenFlyoverP2shErpRedeemScript_shouldReturnTrue() { - Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( - defaultRedeemScriptKeys, - emergencyRedeemScriptKeys, CSV_VALUE); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( - FLYOVER_DERIVATION_HASH.getBytes(), - redeemScript - ).getChunks(); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); + List flyoverP2shErpRedeemScriptChunks = flyoverP2shErpRedeemScript.getChunks(); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverP2shErpRedeemScriptChunks)); } @Test public void hasFlyoverRedeemScriptStructure_whenFlyoverNonStandardErpRedeemScriptParserHardcoded_shouldReturnTrue() { - Script nonStandardErpTestnetRedeemScript = new Script(NON_STANDARD_ERP_TESTNET_REDEEM_SCRIPT_SERIALIZED); - List flyoverRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( + List flyoverNonStandardErpTestnetRedeemScriptChunks = RedeemScriptUtils.createFlyoverRedeemScript( FLYOVER_DERIVATION_HASH.getBytes(), nonStandardErpTestnetRedeemScript ).getChunks(); - Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverRedeemScriptChunks)); + Assert.assertTrue(RedeemScriptValidator.hasFlyoverRedeemScriptStructure(flyoverNonStandardErpTestnetRedeemScriptChunks)); } @Test(expected = VerificationException.class) public void removeOpCheckMultiSig_whenNonStandardErpRedeemScript_ok() { - Script redeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( + Script nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( defaultRedeemScriptKeys, emergencyRedeemScriptKeys, 500L ); - RedeemScriptValidator.removeOpCheckMultisig(redeemScript); + RedeemScriptValidator.removeOpCheckMultisig(nonStandardErpRedeemScript); } @Test public void removeOpCheckMultiSig_standard_redeem_script() { - Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(defaultRedeemScriptKeys); - List chunks = RedeemScriptValidator.removeOpCheckMultisig(redeemScript); + List chunksWithoutOpCheckMultiSig = RedeemScriptValidator.removeOpCheckMultisig(standardRedeemScript); - Assert.assertEquals(defaultRedeemScriptKeys.size() + 2, chunks.size()); // 1 chunk per key + OP_M + OP_N - Assert.assertFalse(RedeemScriptValidator.isRedeemLikeScript(chunks)); - } - - @Test - public void isOpN_valid_opcode() { - ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_2, null); - Assert.assertTrue(RedeemScriptValidator.isOpN(chunk)); - } - - @Test - public void isOpnN_invalid_opcode() { - ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_DROP, null); - Assert.assertFalse(RedeemScriptValidator.isOpN(chunk)); + Assert.assertEquals(defaultRedeemScriptKeys.size() + 2, chunksWithoutOpCheckMultiSig.size()); // 1 chunk per key + OP_M + OP_N + Assert.assertFalse(RedeemScriptValidator.isRedeemLikeScript(chunksWithoutOpCheckMultiSig)); } @Test @@ -580,9 +525,9 @@ public void hasP2shErpRedeemScriptStructure_whenScriptSig_shouldReturnFalse() { p2shErpRedeemScript ); Script scriptSig = p2SHOutputScript.createEmptyInputScript(null, p2shErpRedeemScript); + List scriptSigChunks = scriptSig.getChunks(); - Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - scriptSig.getChunks())); + Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure(scriptSigChunks)); } @Test @@ -590,38 +535,43 @@ public void hasP2shErpRedeemScriptStructure_whenP2shOutputScript_shouldReturnFal Script p2SHOutputScript = ScriptBuilder.createP2SHOutputScript( p2shErpRedeemScript ); + List p2shOutputScriptChunks = p2SHOutputScript.getChunks(); - Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - p2SHOutputScript.getChunks())); + Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure(p2shOutputScriptChunks)); } @Test public void hasP2shErpRedeemScriptStructure_whenStandardRedeemScript_shouldReturnFalse() { + List standardRedeemScriptChunks = standardRedeemScript.getChunks(); Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - standardRedeemScript.getChunks())); + standardRedeemScriptChunks)); } @Test public void hasP2shErpRedeemScriptStructure_whenNonStandardErpRedeemScript_shouldReturnFalse() { + List nonStandardErpRedeemScriptChunks = nonStandardErpRedeemScript.getChunks(); Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - nonstandardErpRedeemScript.getChunks())); + nonStandardErpRedeemScriptChunks)); } @Test public void hasP2shErpRedeemScriptStructure_whenNonStandardErpRedeemScriptParserHardcoded_shouldReturnFalse() { + List nonStandardErpTestnetRedeemScriptChunks = nonStandardErpTestnetRedeemScript.getChunks(); Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - nonStandardErpTestnetRedeemScript.getChunks())); + nonStandardErpTestnetRedeemScriptChunks)); } @Test public void hasP2shErpRedeemScriptStructure_whenFlyoverP2shRedeemScript_shouldReturnFalse() { + List flyoverP2shErpRedeemScriptChunks = flyoverP2shErpRedeemScript.getChunks(); Assert.assertFalse(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - flyoverP2shErpRedeemScript.getChunks())); + flyoverP2shErpRedeemScriptChunks)); } @Test public void hasP2shErpRedeemScriptStructure_whenP2shRedeemScript_shouldReturnTrue() { + List p2shErpRedeemScriptChunks = p2shErpRedeemScript.getChunks(); Assert.assertTrue(RedeemScriptValidator.hasP2shErpRedeemScriptStructure( - p2shErpRedeemScript.getChunks())); + p2shErpRedeemScriptChunks)); } } diff --git a/src/test/java/co/rsk/bitcoinj/script/ScriptBuilderTest.java b/src/test/java/co/rsk/bitcoinj/script/ScriptBuilderTest.java new file mode 100644 index 000000000..7bf6d2794 --- /dev/null +++ b/src/test/java/co/rsk/bitcoinj/script/ScriptBuilderTest.java @@ -0,0 +1,126 @@ +package co.rsk.bitcoinj.script; + +import co.rsk.bitcoinj.core.BtcECKey; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class ScriptBuilderTest { + @Test + public void createMultiSigOutputScript_withTwentyPubKeys_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 20; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test + public void createMultiSigOutputScript_withFifteenPubKeys_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 15; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test(expected = IllegalArgumentException.class) + public void createMultiSigOutputScript_withZeroPubKeys_shouldThrowAnException() { + // Arrange + ArrayList emptyKeys = new ArrayList<>(); + int expectedThreshold = 1; + + // Act & Assert + ScriptBuilder.createMultiSigOutputScript(expectedThreshold, emptyKeys); + } + + @Test(expected = IllegalArgumentException.class) + public void createMultiSigOutputScript_withLessKeysThanTheThreshold_shouldThrowAnException() { + // Arrange + List ecKeys = RedeemScriptUtils.getNKeys(1); + int expectedThreshold = 2; + + // Act & Assert + ScriptBuilder.createMultiSigOutputScript(expectedThreshold, ecKeys); + } + + @Test + public void createMultiSigOutputScript_withOnePubKey_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 1; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test + public void createMultiSigOutputScript_withTenPubKeys_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 10; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test + public void createMultiSigOutputScript_withSixteenPubKeys_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 16; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test + public void createMultiSigOutputScript_with20PubKeys_shouldReturnAValidScript() { + // Arrange + int numberOfKeys = 20; + + // Act & Assert + assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(numberOfKeys); + } + + @Test(expected = IllegalArgumentException.class) + public void createMultiSigOutputScript_withMoreThan20PubKeys_shouldThrowAnException() { + // Arrange + List ecKeys = RedeemScriptUtils.getNKeys(21); + int expectedThreshold = 11; + + // Act & Assert + ScriptBuilder.createMultiSigOutputScript(expectedThreshold, ecKeys); + } + + private static void assertGivenNumberOfKeysCreatesAValidMultiSigOutputScript(int numberOfKeys) { + List ecKeys = RedeemScriptUtils.getNKeys(numberOfKeys); + int expectedThreshold = numberOfKeys / 2 + 1; + + Script multiSigOutputScript = ScriptBuilder.createMultiSigOutputScript(expectedThreshold, ecKeys); + assertTrue(multiSigOutputScript.isSentToMultiSig()); + + // threshold (1) + pubkeys (numberOfKeys) + num of pubKeys (1) + OP_CHECKMULTISIG (1) + int expectedNumberOfChunks = numberOfKeys + 3; + List chunks = multiSigOutputScript.getChunks(); + assertEquals(expectedNumberOfChunks, chunks.size()); + + int index = 0; + int actualThreshold = chunks.get(index++).decodePositiveN(); + assertEquals(expectedThreshold, actualThreshold); + + List pubKeys = ecKeys.stream().map(BtcECKey::getPubKey).collect(Collectors.toList()); + for (int i = 0; i < pubKeys.size(); i++) { + byte[] actualPubKeyInTheScript = chunks.get(i + 1).data; + assertArrayEquals(pubKeys.get(i), actualPubKeyInTheScript); + index++; + } + + int actualTotalKeysNumber = chunks.get(index++).decodePositiveN(); + assertEquals(pubKeys.size(), actualTotalKeysNumber); + + int actualMultiSigOpCode = chunks.get(index).opcode; + assertEquals(ScriptOpCodes.OP_CHECKMULTISIG, actualMultiSigOpCode); + } +} diff --git a/src/test/java/co/rsk/bitcoinj/script/ScriptChunkTest.java b/src/test/java/co/rsk/bitcoinj/script/ScriptChunkTest.java index 0fe32528e..b0528dc5e 100644 --- a/src/test/java/co/rsk/bitcoinj/script/ScriptChunkTest.java +++ b/src/test/java/co/rsk/bitcoinj/script/ScriptChunkTest.java @@ -19,7 +19,9 @@ import static co.rsk.bitcoinj.script.ScriptOpCodes.OP_PUSHDATA1; import static co.rsk.bitcoinj.script.ScriptOpCodes.OP_PUSHDATA2; import static co.rsk.bitcoinj.script.ScriptOpCodes.OP_PUSHDATA4; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -46,4 +48,190 @@ public void testShortestPossibleDataPush() { 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()); } + + @Test + public void isOpCheckMultiSig_withNullData_returnsTrue() { + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null); + assertTrue(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIGVERIFY, null); + assertTrue(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withEmptyData_returnsTrue() { + byte[] emptyData = new byte[]{}; + + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, emptyData); + assertTrue(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIGVERIFY, emptyData); + assertTrue(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withData_returnsTrue() { + byte[] randomData = new byte[]{1, 2, 3}; + + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, randomData); + assertTrue(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIGVERIFY, randomData); + assertTrue(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withNonOpCheckMultiSig_nullData_returnsFalse() { + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_RETURN, null); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_0, null); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_1, null); + assertFalse(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withNonOpCheckMultiSig_emptyData_returnsFalse() { + byte[] emptyData = new byte[]{}; + + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, emptyData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_RETURN, emptyData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_0, emptyData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_1, emptyData); + assertFalse(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withNonOpCheckMultiSig_withData_returnsFalse() { + byte[] randomData = new byte[]{1, 2, 3}; + + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, randomData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_RETURN, randomData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_0, randomData); + assertFalse(chunk.isOpCheckMultiSig()); + + chunk = new ScriptChunk(ScriptOpCodes.OP_1, randomData); + assertFalse(chunk.isOpCheckMultiSig()); + } + + @Test + public void isOpCheckMultiSig_withNonOpCode_returnsFalse() { + ScriptChunk chunk = new ScriptChunk(OP_PUSHDATA1, null); + assertFalse(chunk.isOpCode()); + assertFalse(chunk.isOpCheckMultiSig()); + } + + @Test + public void decodePositiveN_withPositiveSmallNumber_returnsExpectedNumber() { + for (int i=1; i<16; i++) { + ScriptBuilder builder = new ScriptBuilder(); + builder.number(i); + Script script = builder.build(); + + ScriptChunk chunk = script.chunks.get(0); + assertTrue(chunk.isPositiveN()); + assertEquals(i, chunk.decodePositiveN()); + } + } + + @Test + public void decodePositiveN_withPositiveNumber_returnsExpectedNumber() { + for (int n=0; n<20; n++) { // technically we could go up to Integer.MAX_VALUE, but it's not worth it to go that far + int i = 1 << n; // i = 2^n + ScriptBuilder builder = new ScriptBuilder(); + builder.number(i); + Script script = builder.build(); + + ScriptChunk chunk = script.chunks.get(0); + assertTrue(chunk.isPositiveN()); + assertEquals(i, chunk.decodePositiveN()); + } + } + + @Test + public void decodePositiveN_forZero_throwsIAE() { + ScriptBuilder builder = new ScriptBuilder(); + int zero = 0; + builder.number(zero); + Script script = builder.build(); + + ScriptChunk chunk = script.chunks.get(0); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + } + + @Test + public void decodePositiveN_withNegativeNumber_throwsIAE() { + for (int n=0; n<20; n++) { // technically we could go down to Integer.MIN_VALUE, but it's not worth it to go that deep + int i = -(1 << n); // i = -(2^n) + ScriptBuilder builder = new ScriptBuilder(); + builder.number(i); + Script script = builder.build(); + + ScriptChunk chunk = script.chunks.get(0); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + } + } + + @Test + public void decodePositiveN_withNumberLargerThan4Bytes_throwsIAE() { + long i = 1L << 32; + ScriptBuilder builder = new ScriptBuilder(); + builder.number(i); + Script script = builder.build(); + + ScriptChunk chunk = script.chunks.get(0); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + } + + @Test + public void decodePositiveN_withNullPushData_throwsIAE() { + ScriptChunk chunk = new ScriptChunk(OP_PUSHDATA1, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + + chunk = new ScriptChunk(OP_PUSHDATA2, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + + chunk = new ScriptChunk(OP_PUSHDATA4, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + } + + @Test + public void decodePositiveN_withWrongOpcodes_throwsIAE() { + ScriptChunk chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + + chunk = new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + + chunk = new ScriptChunk(ScriptOpCodes.OP_RETURN, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + + chunk = new ScriptChunk(ScriptOpCodes.OP_DROP, null); + assertFalse(chunk.isPositiveN()); + assertThrows(IllegalArgumentException.class, chunk::decodePositiveN); + } } diff --git a/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java b/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java index 223604aea..ba5edd5a9 100644 --- a/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java +++ b/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java @@ -23,21 +23,8 @@ import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.*; -import co.rsk.bitcoinj.core.Address; -import co.rsk.bitcoinj.core.BtcECKey; -import co.rsk.bitcoinj.core.BtcTransaction; +import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.core.BtcTransaction.SigHash; -import co.rsk.bitcoinj.core.Coin; -import co.rsk.bitcoinj.core.MessageSerializer; -import co.rsk.bitcoinj.core.NetworkParameters; -import co.rsk.bitcoinj.core.ScriptException; -import co.rsk.bitcoinj.core.Sha256Hash; -import co.rsk.bitcoinj.core.TransactionInput; -import co.rsk.bitcoinj.core.TransactionOutPoint; -import co.rsk.bitcoinj.core.TransactionOutput; -import co.rsk.bitcoinj.core.UnsafeByteArrayOutputStream; -import co.rsk.bitcoinj.core.Utils; -import co.rsk.bitcoinj.core.VerificationException; import co.rsk.bitcoinj.crypto.TransactionSignature; import co.rsk.bitcoinj.params.MainNetParams; import co.rsk.bitcoinj.script.Script.ScriptType; @@ -52,8 +39,6 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.stream.Collectors; - import org.hamcrest.core.IsNot; import org.junit.Assert; import org.junit.Test; @@ -809,6 +794,261 @@ public void isSentToMultiSig_whenNonStandardErpRedeemScript_shouldReturnTrue() { assertScriptIsMultiSig(nonStandardErpRedeemScript); } + @Test + public void getSigsPrefixCount_whenP2PKOutputScript_shouldReturnZero() { + BtcECKey signer = FEDERATION_KEYS.get(0); + Script p2pkScript = ScriptBuilder.createOutputScript(signer); + assertEquals(0, p2pkScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenP2PKOutputScript_shouldReturnZero() { + BtcECKey signer = FEDERATION_KEYS.get(0); + Script p2pkScript = ScriptBuilder.createOutputScript(signer); + assertEquals(0, p2pkScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenMultiSigOutputScript_shouldReturnOne() { + BtcECKey signer = FEDERATION_KEYS.get(0); + BtcECKey signer2 = FEDERATION_KEYS.get(1); + Script multisigOutputScript = ScriptBuilder.createMultiSigOutputScript(2, Arrays.asList(signer, signer2)); + assertEquals(1, multisigOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenMultiSigOutputScript_shouldReturnZero() { + BtcECKey signer = FEDERATION_KEYS.get(0); + BtcECKey signer2 = FEDERATION_KEYS.get(1); + Script multisigOutputScript = ScriptBuilder.createMultiSigOutputScript(2, Arrays.asList(signer, signer2)); + assertEquals(0, multisigOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenP2shMultisigOutputScript_shouldReturnOne() { + BtcECKey signer = FEDERATION_KEYS.get(0); + BtcECKey signer2 = FEDERATION_KEYS.get(1); + Script p2shMultisigOutputScript = ScriptBuilder.createP2SHOutputScript(2, Arrays.asList(signer, signer2)); + assertEquals(1, p2shMultisigOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenP2shMultisigOutputScript_shouldReturnOne() { + BtcECKey signer = FEDERATION_KEYS.get(0); + BtcECKey signer2 = FEDERATION_KEYS.get(1); + Script p2shMultisigOutputScript = ScriptBuilder.createP2SHOutputScript(2, Arrays.asList(signer, signer2)); + assertEquals(1, p2shMultisigOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenP2shErpRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script p2shErpRedeemScript = createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(p2shErpRedeemScript); + + assertEquals(1, p2shErpRedeemScript.getSigsPrefixCount()); + assertEquals(1, p2shOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenP2shErpRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script p2shErpRedeemScript = createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + // Actually it should be 2 (redeemScript + OP_NOTIF) + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(p2shErpRedeemScript); + + assertEquals(0, p2shErpRedeemScript.getSigsSuffixCount()); + assertEquals(1, p2shOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenFlyoverStandardRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(FEDERATION_KEYS); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + redeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(1, flyoverRedeemScript.getSigsPrefixCount()); + assertEquals(1, p2shOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenFlyoverStandardRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script redeemScript = RedeemScriptUtils.createStandardRedeemScript(FEDERATION_KEYS); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + redeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(0, flyoverRedeemScript.getSigsSuffixCount()); + assertEquals(1, p2shOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenFlyoverNonStandardErpRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + nonStandardErpRedeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(1, flyoverRedeemScript.getSigsPrefixCount()); + assertEquals(1, p2shOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenFlyoverNonStandardErpRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script nonStandardErpRedeemScript = RedeemScriptUtils.createNonStandardErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + nonStandardErpRedeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(0, flyoverRedeemScript.getSigsSuffixCount()); + assertEquals(1, p2shOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenFlyoverP2shErpRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + redeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(1, flyoverRedeemScript.getSigsPrefixCount()); + assertEquals(1, p2shOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenFlyoverP2shErpRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script redeemScript = RedeemScriptUtils.createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script flyoverRedeemScript = RedeemScriptUtils.createFlyoverRedeemScript( + FLYOVER_DERIVATION_HASH, + redeemScript + ); + + Script p2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + + assertEquals(0, flyoverRedeemScript.getSigsSuffixCount()); + assertEquals(1, p2shOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenStandardRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script standardRedeemScript = RedeemScriptUtils.createStandardRedeemScript(FEDERATION_KEYS); + Script standardP2SHOutputScript = ScriptBuilder.createP2SHOutputScript(standardRedeemScript); + + assertEquals(1, standardRedeemScript.getSigsPrefixCount()); + assertEquals(1, standardP2SHOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenStandardRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script standardRedeemScript = RedeemScriptUtils.createStandardRedeemScript(FEDERATION_KEYS); + Script standardP2SHOutputScript = ScriptBuilder.createP2SHOutputScript(standardRedeemScript); + + assertEquals(0, standardRedeemScript.getSigsSuffixCount()); + assertEquals(1, standardP2SHOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenNonStandardRedeemScript_withP2SHOutputScript_shouldBothReturnOne() { + Script nonStandardErpRedeemScript = createNonStandardErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script standardP2SHOutputScript = ScriptBuilder.createP2SHOutputScript(nonStandardErpRedeemScript); + + assertEquals(1, nonStandardErpRedeemScript.getSigsPrefixCount()); + assertEquals(1, standardP2SHOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenNonStandardRedeemScript_withP2SHOutputScript_shouldReturnZeroAndOne() { + Script nonStandardErpRedeemScript = createNonStandardErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script standardP2SHOutputScript = ScriptBuilder.createP2SHOutputScript(nonStandardErpRedeemScript); + + assertEquals(0, nonStandardErpRedeemScript.getSigsSuffixCount()); + assertEquals(1, standardP2SHOutputScript.getSigsSuffixCount()); + } + + @Test + public void getSigsPrefixCount_whenStandardRedeemScript_withP2SHP2WSHOutputScript_shouldReturnOne() { + Script standardErpRedeemScript = createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script standardP2SHP2WSHOutputScript = ScriptBuilder.createP2SHP2WSHOutputScript(standardErpRedeemScript); + + assertEquals(1, standardErpRedeemScript.getSigsPrefixCount()); + assertEquals(1, standardP2SHP2WSHOutputScript.getSigsPrefixCount()); + } + + @Test + public void getSigsSuffixCount_whenStandardRedeemScript_withP2SHP2WSHOutputScript_shouldReturnOne() { + Script standardErpRedeemScript = createP2shErpRedeemScript( + FEDERATION_KEYS, + ERP_FEDERATION_KEYS, + CSV_VALUE + ); + + Script standardP2SHP2WSHOutputScript = ScriptBuilder.createP2SHP2WSHOutputScript(standardErpRedeemScript); + + assertEquals(0, standardErpRedeemScript.getSigsSuffixCount()); + assertEquals(1, standardP2SHP2WSHOutputScript.getSigsSuffixCount()); + } + @Test public void isSentToMultiSig_whenP2shErpRedeemScript_shouldReturnTrue() { Script p2shErpRedeemScript = createP2shErpRedeemScript( diff --git a/src/test/java/co/rsk/bitcoinj/wallet/WalletTest.java b/src/test/java/co/rsk/bitcoinj/wallet/WalletTest.java index 929a890fc..274d58c36 100644 --- a/src/test/java/co/rsk/bitcoinj/wallet/WalletTest.java +++ b/src/test/java/co/rsk/bitcoinj/wallet/WalletTest.java @@ -1,7 +1,6 @@ package co.rsk.bitcoinj.wallet; import co.rsk.bitcoinj.core.*; -import co.rsk.bitcoinj.params.UnitTestParams; import co.rsk.bitcoinj.script.Script; import co.rsk.bitcoinj.script.ScriptBuilder; import org.junit.Test; @@ -10,24 +9,13 @@ import java.nio.charset.StandardCharsets; import java.util.*; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class WalletTest { // data from tx https://mempool.space/testnet/tx/1744459aeaf7369aadc9fc40de9ab2bf575b14e35029b35a7ee4bbd3de65af7f private static final NetworkParameters TESTNET = NetworkParameters.fromID(NetworkParameters.ID_TESTNET); private static final Address ADDRESS_TO = Address.fromBase58(TESTNET, "mwUXQVdcCwCeYJx7mBhH4yLBU5N6QDSBZK"); - - private static final BtcECKey PUBLIC_KEY_1 = BtcECKey.fromPublicOnly(Hex.decode("027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d3561007")); - private static final BtcECKey PUBLIC_KEY_2 = BtcECKey.fromPublicOnly(Hex.decode("02d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856")); - private static final BtcECKey PUBLIC_KEY_3 = BtcECKey.fromPublicOnly(Hex.decode("0346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e")); - private static final List PUBLIC_KEYS = Arrays.asList(PUBLIC_KEY_1, PUBLIC_KEY_2, PUBLIC_KEY_3); - private static final Script REDEEM_SCRIPT = new Script( - Hex.decode("5221027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d35610072102d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856210346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e53ae") - ); - - private static final Sha256Hash UTXO_HASH = Sha256Hash.of("hash".getBytes(StandardCharsets.UTF_8)); - private static final Coin AVAILABLE_AMOUNT = Coin.FIFTY_COINS; + private static List PUBLIC_KEYS; // CoinSelector and UTXOProvider are just interfaces, // so rskj has its own implementations for them. @@ -47,6 +35,11 @@ public class WalletTest { return new CoinSelection(Coin.valueOf(total), selected); }; + private static Script REDEEM_SCRIPT; + private static int AMOUNT_OF_UTXOS_TO_USE; + private static Coin AMOUNT_IN_EACH_UTXO; + private static Coin VALUE_TO_SEND; + private Wallet wallet; private BtcTransaction tx; private SendRequest sr; @@ -63,29 +56,36 @@ public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) { }; wallet.setCoinSelector(COIN_SELECTOR); - UTXOProvider utxoProvider = getUtxoProvider(scriptPubKey); + UTXOProvider utxoProvider = getUtxosProvider(scriptPubKey); wallet.setUTXOProvider(utxoProvider); Address address = Address.fromP2SHScript(TESTNET, scriptPubKey); wallet.addWatchedAddress(address); tx = new BtcTransaction(TESTNET); - Coin valueToSend = Coin.FIFTY_COINS; - tx.addOutput(valueToSend, ADDRESS_TO); + tx.addOutput(VALUE_TO_SEND, ADDRESS_TO); sr = SendRequest.forTx(tx); sr.signInputs = false; sr.recipientsPayFees = true; sr.feePerKb = Coin.valueOf(10000L); + sr.changeAddress = Address.fromP2SHScript(TESTNET, scriptPubKey); + sr.shuffleOutputs = false; } - private static UTXOProvider getUtxoProvider(Script scriptPubKey) { - UTXO utxo = new UTXO(UTXO_HASH, 0, AVAILABLE_AMOUNT, 10, false, scriptPubKey); + private static UTXOProvider getUtxosProvider(Script scriptPubKey) { + List utxos = new ArrayList<>(); + for (int i = 0; i < AMOUNT_OF_UTXOS_TO_USE; i++) { + String hash = "hash" + "i"; + Sha256Hash utxoHash = Sha256Hash.of(hash.getBytes(StandardCharsets.UTF_8)); + UTXO utxo = new UTXO(utxoHash, i, AMOUNT_IN_EACH_UTXO, 10, false, scriptPubKey); + utxos.add(utxo); + } return new UTXOProvider() { @Override public List getOpenTransactionOutputs(List
addresses) { - return Collections.singletonList(utxo); + return utxos; } @Override @@ -101,9 +101,20 @@ public NetworkParameters getParams() { } @Test - public void completeTx_legacy() throws InsufficientMoneyException { + public void completeTx_legacyTx_shouldCalculateTxSizeCorrectly() throws InsufficientMoneyException { // arrange + final BtcECKey publicKey1 = BtcECKey.fromPublicOnly(Hex.decode("027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d3561007")); + final BtcECKey publicKey2 = BtcECKey.fromPublicOnly(Hex.decode("02d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856")); + final BtcECKey publicKey3 = BtcECKey.fromPublicOnly(Hex.decode("0346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e")); + PUBLIC_KEYS = Arrays.asList(publicKey1, publicKey2, publicKey3); + REDEEM_SCRIPT = new Script( + Hex.decode("5221027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d35610072102d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856210346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e53ae") + ); Script legacyScriptPubKey = ScriptBuilder.createP2SHOutputScript(REDEEM_SCRIPT); + + VALUE_TO_SEND = Coin.FIFTY_COINS; + AMOUNT_OF_UTXOS_TO_USE = 1; + AMOUNT_IN_EACH_UTXO = Coin.FIFTY_COINS; setUp(legacyScriptPubKey); // act @@ -111,17 +122,29 @@ public void completeTx_legacy() throws InsufficientMoneyException { wallet.completeTx(sr); // assert - double expectedSize = 375; + double realSize = 375; + int expectedCalculatedSize = 340; // for legacy tx, the calculation has a 10% diff approx, // but we have to keep the same implementation for backwards compatibility double allowedPercentageError = 10.0; - assertCalculatedSizeIsCloseToExpectedSize(expectedSize, allowedPercentageError); + assertCalculatedSizeIsCloseToExpectedSize(realSize, allowedPercentageError, expectedCalculatedSize); } @Test - public void completeTx_segwit() throws InsufficientMoneyException { + public void completeTx_segwitTx_shouldCalculateTxSizeCorrectly() throws InsufficientMoneyException { // arrange + final BtcECKey publicKey1 = BtcECKey.fromPublicOnly(Hex.decode("027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d3561007")); + final BtcECKey publicKey2 = BtcECKey.fromPublicOnly(Hex.decode("02d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856")); + final BtcECKey publicKey3 = BtcECKey.fromPublicOnly(Hex.decode("0346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e")); + PUBLIC_KEYS = Arrays.asList(publicKey1, publicKey2, publicKey3); + REDEEM_SCRIPT = new Script( + Hex.decode("5221027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d35610072102d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856210346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e53ae") + ); Script scriptPubKey = ScriptBuilder.createP2SHP2WSHOutputScript(REDEEM_SCRIPT); + + VALUE_TO_SEND = Coin.FIFTY_COINS; + AMOUNT_OF_UTXOS_TO_USE = 1; + AMOUNT_IN_EACH_UTXO = Coin.FIFTY_COINS; setUp(scriptPubKey); // act @@ -129,22 +152,69 @@ public void completeTx_segwit() throws InsufficientMoneyException { wallet.completeTx(sr); // assert - double expectedSize = 183.75; + double realSize = 183.75; + int expectedCalculatedSize = 184; // for segwit, the calculation seems to be pretty accurate double allowedPercentageError = 1.0; - assertCalculatedSizeIsCloseToExpectedSize(expectedSize, allowedPercentageError); + assertCalculatedSizeIsCloseToExpectedSize(realSize, allowedPercentageError, expectedCalculatedSize); + } + + @Test + public void completeTx_forTxThatExceedsMaximumStandardSize_throwsExceededMaxTransactionSizeException() { + // inspired from tx https://mempool.space/testnet/tx/2cadd21b8b5a188afed27754a3f1a97324bd45ac8ec61ce57ff79e90b9fad8a7 + + // arrange + final BtcECKey publicKey1 = BtcECKey.fromPublicOnly(Hex.decode("0211310637a4062844098b46278059298cc948e1ff314ca9ec75c82e0d0b8ad22c")); + final BtcECKey publicKey2 = BtcECKey.fromPublicOnly(Hex.decode("0238de69e208565fd82e4b76c4eff5d817a51679b8a90c41709e49660ba23501c5")); + final BtcECKey publicKey3 = BtcECKey.fromPublicOnly(Hex.decode("024b120731b26ec7165cddd214fc8e3f0c844a03dc0e533fb0cf9f89ad2f68a881")); + final BtcECKey publicKey4 = BtcECKey.fromPublicOnly(Hex.decode("0274564db76110474ac0d7e09080c182855b22a864cc201ed55217b23301f52f22")); + final BtcECKey publicKey5 = BtcECKey.fromPublicOnly(Hex.decode("02867f0e693a2553bf2bc13a5efa0b516b28e66317fbe8e484dd3f375bcb48ec59")); + final BtcECKey publicKey6 = BtcECKey.fromPublicOnly(Hex.decode("02881af2910c909f224557353dd28e3729363cf5c24232f26d25c92dac72a3dcdb")); + final BtcECKey publicKey7 = BtcECKey.fromPublicOnly(Hex.decode("029c75b3257e0842c4be48e57e39bf2735c74a76c4b1c0b08d1cc66bf5b8748cc1")); + final BtcECKey publicKey8 = BtcECKey.fromPublicOnly(Hex.decode("02a46cbe93287cb51a398a157de2b428f21a94f46affdd916ce921bd10db652033")); + final BtcECKey publicKey9 = BtcECKey.fromPublicOnly(Hex.decode("02d335ef4eeb74330c3a53f529f9741fa096412c7982ed681fcf69763894f34f89")); + final BtcECKey publicKey10 = BtcECKey.fromPublicOnly(Hex.decode("02d3f5fd6e107cf68b1be8dce0e16a0a8afb8dcef9a76c851d7eaf6d51c46a3575")); + final BtcECKey publicKey11 = BtcECKey.fromPublicOnly(Hex.decode("03163b86a62b4eeeb52f67cb16ce13a8622a066f2a063280749b956a97705dfc3d")); + final BtcECKey publicKey12 = BtcECKey.fromPublicOnly(Hex.decode("033267e382e076cbaa199d49ea7362535f95b135de181caf66b391f541bf39ab0e")); + final BtcECKey publicKey13 = BtcECKey.fromPublicOnly(Hex.decode("0343e106d90183e2eef7d5cb7538a634439bf1301d731787c6736922ff19e750ed")); + final BtcECKey publicKey14 = BtcECKey.fromPublicOnly(Hex.decode("034461d4263b907cfc5ebb468f19d6a133b567f3cc4855e8725faaf60c6e388bca")); + final BtcECKey publicKey15 = BtcECKey.fromPublicOnly(Hex.decode("036e92e6555d2e70af4f5a4f888145356e60bb1a5bc00786a8e9f50152090b2f69")); + final BtcECKey publicKey16 = BtcECKey.fromPublicOnly(Hex.decode("03ab54da6b69407dcaaa85f6904687052c93f1f9dd0633f1321b3e624fcd30144b")); + final BtcECKey publicKey17 = BtcECKey.fromPublicOnly(Hex.decode("03bd5b51b1c5d799da190285c8078a2712b8e5dc6f73c799751e6256bb89a4bd04")); + final BtcECKey publicKey18 = BtcECKey.fromPublicOnly(Hex.decode("03be060191c9632184f2a0ab2638eeed04399372f37fc7a3cff5291cfd6426cf35")); + final BtcECKey publicKey19 = BtcECKey.fromPublicOnly(Hex.decode("03e6def9ef0597336eb58d24f955b6b63756cf7b3885322f9d0cf5a2a12f7e459b")); + final BtcECKey publicKey20 = BtcECKey.fromPublicOnly(Hex.decode("03ef03253b7b4f33d68c39141eb016df15fafbb1d0fa4a2e7f208c94ea154ab8c3")); + PUBLIC_KEYS = Arrays.asList( + publicKey1, publicKey2, publicKey3, publicKey4, publicKey5, publicKey6, publicKey7, publicKey8, publicKey9, publicKey10, + publicKey11, publicKey12, publicKey13, publicKey14, publicKey15, publicKey16, publicKey17, publicKey18, publicKey19, publicKey20 + ); + byte[] rawRedeemScript = Hex.decode("645b210211310637a4062844098b46278059298cc948e1ff314ca9ec75c82e0d0b8ad22c210238de69e208565fd82e4b76c4eff5d817a51679b8a90c41709e49660ba23501c521024b120731b26ec7165cddd214fc8e3f0c844a03dc0e533fb0cf9f89ad2f68a881210274564db76110474ac0d7e09080c182855b22a864cc201ed55217b23301f52f222102867f0e693a2553bf2bc13a5efa0b516b28e66317fbe8e484dd3f375bcb48ec592102881af2910c909f224557353dd28e3729363cf5c24232f26d25c92dac72a3dcdb21029c75b3257e0842c4be48e57e39bf2735c74a76c4b1c0b08d1cc66bf5b8748cc12102a46cbe93287cb51a398a157de2b428f21a94f46affdd916ce921bd10db6520332102d335ef4eeb74330c3a53f529f9741fa096412c7982ed681fcf69763894f34f892102d3f5fd6e107cf68b1be8dce0e16a0a8afb8dcef9a76c851d7eaf6d51c46a35752103163b86a62b4eeeb52f67cb16ce13a8622a066f2a063280749b956a97705dfc3d21033267e382e076cbaa199d49ea7362535f95b135de181caf66b391f541bf39ab0e210343e106d90183e2eef7d5cb7538a634439bf1301d731787c6736922ff19e750ed21034461d4263b907cfc5ebb468f19d6a133b567f3cc4855e8725faaf60c6e388bca21036e92e6555d2e70af4f5a4f888145356e60bb1a5bc00786a8e9f50152090b2f692103ab54da6b69407dcaaa85f6904687052c93f1f9dd0633f1321b3e624fcd30144b2103bd5b51b1c5d799da190285c8078a2712b8e5dc6f73c799751e6256bb89a4bd042103be060191c9632184f2a0ab2638eeed04399372f37fc7a3cff5291cfd6426cf352103e6def9ef0597336eb58d24f955b6b63756cf7b3885322f9d0cf5a2a12f7e459b2103ef03253b7b4f33d68c39141eb016df15fafbb1d0fa4a2e7f208c94ea154ab8c30114ae67011eb2755b21021a560245f78312588f600315d75d493420bed65873b63d0d4bb8ca1b9163a35b2102218e9dc07ac4190a1d7df94fc75953b36671129f12668a94f1f504fe47399ead210272ed6e14e70f6b4757d412729730837bc63b6313276be8308a5a96afd63af9942102872f69892a74d60f6185c2908414dcddb24951c035a1a8466c6c56f55043e7602102886d7d8e865f75dfda3ddf94619af87ad8aa71e8ef393e1e57593576b7d7af1621028e59462fb53ba31186a353b7ea77ebefda9097392e45b7ca7a216168230d05af21028f5a88b08d75765b36951254e68060759de5be7e559972c37c67fc8cedafeb262102c9ced4bbc468af9ace1645df2fd50182d5822cb4c68aae0e50ae1d45da260d2a2102deba35a96add157b6de58f48bb6e23bcb0a17037bed1beb8ba98de6b0a0d71d62102f2e00fefa5868e2c56405e188ec1d97557a7c77fb6a448352cc091c2ae9d50492102fb8c06c723d4e59792e36e6226087fcfac65c1d8a0d5c5726a64102a551528442103077c62a45ea1a679e54c9f7ad800d8e40eaf6012657c8dccd3b61d5e070d9a432103616959a72dd302043e9db2dbd7827944ecb2d555a8f72a48bb8f916ec5aac6ec210362f9c79cd0586704d6a9ea863573f3b123d90a31faaa5a1d9a69bf9631c78ae321036899d94ad9d3f24152dd4fa79b9cb8dddbd26d18297be4facb295f57c9de60bd210376e4cb35baa8c46b0dcffaf303785c5f7aadf457df30ac956234cc8114e2f47d2103a587256beec4e167aebc478e1d6502bb277a596ae9574ccb646da11fffbf36502103bb9da162c3f581ced93167f86d7e0e5962762a1188f5bd1f8b5d08fed46ef73d2103c34fcd05cef2733ea7337c37f50ae26245646aba124948c6ff8dcdf8212849982103f8ac768e683a07ac4063f72a6d856aedeae109f844abcfa34ac9519d715177460114ae68"); + REDEEM_SCRIPT = new Script(rawRedeemScript); + Script scriptPubKey = ScriptBuilder.createP2SHP2WSHOutputScript(REDEEM_SCRIPT); + + AMOUNT_OF_UTXOS_TO_USE = 170; + AMOUNT_IN_EACH_UTXO = Coin.valueOf(10_000); + VALUE_TO_SEND = AMOUNT_IN_EACH_UTXO.multiply(AMOUNT_OF_UTXOS_TO_USE); // to have to use all the utxos + + setUp(scriptPubKey); + sr.isSegwitCompatible = true; + + // act & assert + assertThrows(Wallet.ExceededMaxTransactionSize.class, () -> wallet.completeTx(sr)); } - private void assertCalculatedSizeIsCloseToExpectedSize(double expectedSize, double allowedPercentageError) { - double allowedError = Math.ceil(expectedSize * allowedPercentageError / 100); + private void assertCalculatedSizeIsCloseToExpectedSize(double realSize, double allowedPercentageError, int expectedCalculatedSize) { + double allowedError = Math.ceil(realSize * allowedPercentageError / 100); + Coin availableAmount = AMOUNT_IN_EACH_UTXO.multiply(AMOUNT_OF_UTXOS_TO_USE); Coin actualValueSent = tx.getOutputs().get(0).getValue(); - Coin fee = AVAILABLE_AMOUNT.minus(actualValueSent); + Coin fee = availableAmount.minus(actualValueSent); // fee = (tx size * feePerKb) / 1000 => fee = (tx size * 10_000) / 1000 // => fee = tx size * 10 => tx size = fee / 10 long size = fee.divide(10L).getValue(); + assertEquals(expectedCalculatedSize, size); // to make the test deterministic - double diff = Math.abs(expectedSize - size); + double diff = Math.abs(realSize - size); assertTrue(diff <= allowedError); } }