Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
28381cc
Create getSigInsertionIndex and updateWitnessWithSignature methods in…
julia-zack May 8, 2025
8c5aa18
feat: added tests for getSigInsertionIndex
julianlen May 23, 2025
a944fdd
fix: little refactors to make the test TransactionWitnessTest simpler
julianlen May 23, 2025
68a6d74
fix: extracted reused variables as class variables in TxWitnessTest
julianlen May 26, 2025
87c15e4
fix: tiny typo
julianlen May 26, 2025
eaf8a02
fix: redeemScriptHash is actually redeemScriptSerialized
julianlen May 26, 2025
45743e6
fix: replaced declaration of tests to use assertThrows
julianlen May 26, 2025
8480ea5
fix: I was using hashForSignature for the witness, now fixed with has…
julianlen May 26, 2025
a93fdc4
fix: used createP2SHP2WSHOutputScript
julianlen May 26, 2025
b06ff7b
fix: improved variable names for the test getSigInsertionIndex_whenSe…
julianlen May 26, 2025
d97cf0c
fix: cleaning test names, variable names and comments
julianlen May 26, 2025
f0bf9b4
feat: refactored the test because they were using the same methods, s…
julianlen May 26, 2025
de0d8af
fix: merged tests that check the getInsertionIndex with no signatures…
julianlen May 26, 2025
1ed360f
fix: various fixes
julianlen May 27, 2025
9acfaf2
fix: erased unused import
julianlen May 27, 2025
a0ca95d
fix: became a constant as private
julianlen May 27, 2025
4f6cd36
fix: so apparently Before in this jUnit version is BeforeEach, so I a…
julianlen May 27, 2025
dd47b66
fix: little renamings and deleted the method getBtcTransactionWithBas…
julianlen May 27, 2025
d2787df
fix: there was an useless line in the getSigInsertionIndex_withWitnes…
julianlen May 28, 2025
e5a4db5
feat: added tests for updateWitnessWithSignature
julianlen May 28, 2025
ab7d58c
feat: added tests for getSigsSuffix and getSigsPrefix methods
julianlen May 29, 2025
3888804
fix: solved out all the comments
julianlen May 29, 2025
e8aa0d1
fix: clarified in the tests' names whether the script is redeemScript…
julianlen May 29, 2025
970760d
fix: fixed the signers with FED keys to become the tests deterministic
julianlen May 29, 2025
12a5329
Build multisig output script using number instead of smallNum
julia-zack May 13, 2025
f379b83
Move isN() and isOpCheckMultiSig() to ScriptChunk class
marcos-iov May 30, 2025
52cc00c
Move decodeN method to ScriptChunk class
marcos-iov May 30, 2025
eb1ee2f
Add private method to get N value in StandardRedeemScriptParser
marcos-iov May 30, 2025
92403cb
Minor refactors
marcos-iov May 30, 2025
1eab95c
Update version to 0.14.4-rsk-18_3-SNAPSHOT
marcos-iov May 30, 2025
f508ef2
feat: tested the RedeemScriptValidator.hasStandardRedeemScriptStructu…
julianlen Jun 4, 2025
4b4320c
Set version to 0.14.4-rsk-18
marcos-iov Jun 4, 2025
fe984c1
Remove unused import
marcos-iov Jun 4, 2025
00d27a0
fix: little refactor since the field flyoverNonStandardErpRedeemScrip…
julianlen Jun 4, 2025
979ae9d
fix: deleted repeated code
julianlen Jun 4, 2025
327295c
feat: added a test such that it creates a redeem like script but does…
julianlen Jun 4, 2025
fcc5e50
feat: defaultRedeemScriptKeys now returns 20 keys
julianlen Jun 4, 2025
dbce5ed
fix: deleted useless test that tested for 20 keys when all the tests …
julianlen Jun 4, 2025
79cb9a6
fix: getNKeys now is private
julianlen Jun 5, 2025
df54308
feat: added two test cases, one with 50 keys and another with an inva…
julianlen Jun 5, 2025
795f1d3
feat: added a test such that if the number of keys is 20, it creates …
julianlen Jun 6, 2025
12e92ac
fix: constant EXPECTED_M in the FlyoverRedeemScriptParserTest's origi…
julianlen Jun 6, 2025
32612d5
fix: changed chunks indexing
julianlen Jun 6, 2025
bb18cde
feat: added test cases that test the building ofan multisig output sc…
julianlen Jun 10, 2025
b1f8d4a
fix: added the size of the list
julianlen Jun 10, 2025
f6b6916
fix: moved a constant that is used in only one place to the test that…
julianlen Jun 10, 2025
cb92961
fix: moved the new tests to ScriptBuilderTest file
julianlen Jun 10, 2025
48e8904
fix: useless extra blank space
julianlen Jun 10, 2025
c502c58
fix: fixed tests in RedeemScriptValidatorTest
julianlen Jun 10, 2025
f5f2d0b
fix: unified variables names
julianlen Jun 10, 2025
0a7198f
fix: better naming for variables in the RedeemScriptValidatorTest file
julianlen Jun 10, 2025
f694498
fix: changed threshold for expectedThreshold in ScriptBuilderTest
julianlen Jun 10, 2025
a13f506
fix: deleted method getExpectedNumberOfChunks
julianlen Jun 10, 2025
b590a65
feat: added more tests cases (with n= 0,1,9,10, and with n < threshol…
julianlen Jun 10, 2025
de472c8
Update src/test/java/co/rsk/bitcoinj/script/ScriptBuilderTest.java
julianlen Jun 11, 2025
83f4519
Update src/test/java/co/rsk/bitcoinj/script/RedeemScriptValidatorTest…
julianlen Jun 11, 2025
cd249da
fix: ArrayList was misdeclared
julianlen Jun 12, 2025
26b1a05
Modify createMultisigOutputScript to check that pubkeys are leq than 20
julia-zack Jun 25, 2025
2a12964
Decode positive N considering encoding with sign
julia-zack Jun 27, 2025
012dbd7
Add test case for i=0
julia-zack Jun 27, 2025
e379669
Increase powers of n to test a few more numbers
julia-zack Jun 27, 2025
fb9b7dd
Keep isPositiveN method to check whether a chunk is a positive n. Cre…
julia-zack Jul 2, 2025
4d4262d
Add test cases. Better error handling when data is null or larger tha…
julia-zack Jul 2, 2025
2e959d0
Consider segwit size when checking for max standard tx size. Add test
julia-zack Jul 2, 2025
8807a5a
Refactor to avoid code duplication. Add javadocs to methods and remov…
julia-zack Jul 4, 2025
d58223b
Minor refactors to tests
julia-zack Jul 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>co.rsk.bitcoinj</groupId>
<version>0.14.4-rsk-18_2-SNAPSHOT</version>
<version>0.14.4-rsk-18</version>
<artifactId>bitcoinj-thin</artifactId>

<name>bitcoinj-thin</name>
Expand Down
109 changes: 105 additions & 4 deletions src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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<byte[]> 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) {
Expand Down
54 changes: 27 additions & 27 deletions src/main/java/co/rsk/bitcoinj/script/RedeemScriptValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@

public class RedeemScriptValidator {

private RedeemScriptValidator() {
// Prevent instantiation
}

protected static boolean isRedeemLikeScript(List<ScriptChunk> chunks) {
if (chunks.size() < 4) {
return false;
}

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;
}
Expand All @@ -23,9 +25,7 @@ protected static boolean isRedeemLikeScript(List<ScriptChunk> 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<ScriptChunk> chunks) {
Expand All @@ -34,32 +34,38 @@ protected static boolean hasStandardRedeemScriptStructure(List<ScriptChunk> 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;
}
}

return true;
} catch (IllegalStateException e) {
return false; // Not an OP_N opcode.
return false; // Not a number
}
}

Expand All @@ -68,10 +74,11 @@ protected static boolean hasP2shErpRedeemScriptStructure(List<ScriptChunk> 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;
Expand Down Expand Up @@ -101,8 +108,7 @@ protected static boolean hasP2shErpRedeemScriptStructure(List<ScriptChunk> chunk
return false;
}

/***
* Expected structure:
/* The redeem script structure should be as follows:
* OP_NOTIF
* OP_M
* PUBKEYS...N
Expand All @@ -119,7 +125,6 @@ protected static boolean hasP2shErpRedeemScriptStructure(List<ScriptChunk> chunk
* OP_CHECKMULTISIG
* OP_ENDIF
*/

// Validate both default and erp federations redeem scripts.
// Extract the default PowPeg and the emergency multisig redeemscript chunks
List<ScriptChunk> defaultFedRedeemScriptChunks = chunks.subList(1, elseOpcodeIndex);
Expand Down Expand Up @@ -218,9 +223,4 @@ protected static List<ScriptChunk> 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;
}
}
62 changes: 29 additions & 33 deletions src/main/java/co/rsk/bitcoinj/script/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 <sig>* <redeemScript>
sigsSuffixCount = 1;
} else if (isSentToMultiSig()) {
sigsPrefixCount = 1; // OP_0 <sig>*
} else if (isSentToAddress()) {
sigsSuffixCount = 1; // <sig> <pubkey>
}
int sigsPrefixCount = getSigsPrefixCount();
int sigsSuffixCount = getSigsSuffixCount();
return ScriptBuilder.updateScriptWithSignature(scriptSig, sigBytes, index, sigsPrefixCount, sigsSuffixCount);
}

public int getSigsPrefixCount() {
if (isPayToScriptHash() || isSentToMultiSig()) { // OP_0 <sig>* || OP_0 <sig>*
return 1;
}
return 0;
}

public int getSigsSuffixCount() {
if (isPayToScriptHash() || isSentToAddress()) { // <sig>* <redeemScript> || <sig> <pubkey>
return 1;
}
return 0;
}

private RedeemScriptParser getRedeemScriptParser() {
if (redeemScriptParser == null){
redeemScriptParser = RedeemScriptParserFactory.get(chunks);
Expand Down Expand Up @@ -554,22 +548,24 @@ private static int getSigOpCount(List<ScriptChunk> 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;
}
}

/**
Expand Down
Loading