diff --git a/src/main/java/co/rsk/bitcoinj/core/BtcECKey.java b/src/main/java/co/rsk/bitcoinj/core/BtcECKey.java index c28dbd3b2..fbd338b0c 100644 --- a/src/main/java/co/rsk/bitcoinj/core/BtcECKey.java +++ b/src/main/java/co/rsk/bitcoinj/core/BtcECKey.java @@ -432,7 +432,7 @@ public static ECPoint publicPointFromPrivate(BigInteger privKey) { /** Gets the hash160 form of the public key (as seen in addresses). */ public byte[] getPubKeyHash() { if (pubKeyHash == null) - pubKeyHash = Utils.sha256hash160(this.pub.getEncoded()); + pubKeyHash = Utils.hash160(this.pub.getEncoded()); return pubKeyHash; } diff --git a/src/main/java/co/rsk/bitcoinj/core/BtcTransaction.java b/src/main/java/co/rsk/bitcoinj/core/BtcTransaction.java index b0c55c40d..995ed687a 100644 --- a/src/main/java/co/rsk/bitcoinj/core/BtcTransaction.java +++ b/src/main/java/co/rsk/bitcoinj/core/BtcTransaction.java @@ -30,6 +30,7 @@ import com.google.common.primitives.Longs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.params.KeyParameter; import javax.annotation.Nullable; import java.io.*; @@ -105,7 +106,6 @@ public int compare(final BtcTransaction tx1, final BtcTransaction tx2) { private ArrayList outputs; private ArrayList witnesses; - private long lockTime; // This is either the time the transaction was broadcast as measured from the local clock, or the time from the @@ -1166,6 +1166,112 @@ public Sha256Hash hashForSignature(int inputIndex, byte[] connectedScript, byte } } + public synchronized Sha256Hash hashForWitnessSignature( + int inputIndex, + byte[] scriptCode, + Coin prevValue, + SigHash type, + boolean anyoneCanPay) { + int sigHash = TransactionSignature.calcSigHashValue(type, anyoneCanPay); + return hashForWitnessSignature(inputIndex, scriptCode, prevValue, (byte) sigHash); + } + + /** + *

Calculates a signature hash, that is, a hash of a simplified form of the transaction. How exactly the transaction + * is simplified is specified by the type and anyoneCanPay parameters.

+ * + *

This is a low level API and when using the regular {@link Wallet} class you don't have to call this yourself. + * When working with more complex transaction types and contracts, it can be necessary. When signing a Witness output + * the scriptCode should be the script encoded into the scriptSig field, for normal transactions, it's the + * scriptPubKey of the output you're signing for. (See BIP143: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki)

+ * + * @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input. + * @param scriptCode the script that should be in the given input during signing. + * @param prevValue the value of the coin being spent + * @param type Should be SigHash.ALL + * @param anyoneCanPay should be false. + */ + public synchronized Sha256Hash hashForWitnessSignature( + int inputIndex, + Script scriptCode, + Coin prevValue, + SigHash type, + boolean anyoneCanPay) { + return hashForWitnessSignature(inputIndex, scriptCode.getProgram(), prevValue, type, anyoneCanPay); + } + + public synchronized Sha256Hash hashForWitnessSignature( + int inputIndex, + byte[] scriptCode, + Coin prevValue, + byte sigHashType){ + ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(length == UNKNOWN_LENGTH ? 256 : length + 4); + try { + byte[] hashPrevouts = new byte[32]; + byte[] hashSequence = new byte[32]; + byte[] hashOutputs = new byte[32]; + int basicSigHashType = sigHashType & 0x1f; + boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; + boolean signAll = (basicSigHashType != SigHash.SINGLE.value) && (basicSigHashType != SigHash.NONE.value); + + if (!anyoneCanPay) { + ByteArrayOutputStream bosHashPrevouts = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.inputs.size(); ++i) { + bosHashPrevouts.write(this.inputs.get(i).getOutpoint().getHash().getReversedBytes()); + uint32ToByteStreamLE(this.inputs.get(i).getOutpoint().getIndex(), bosHashPrevouts); + } + hashPrevouts = Sha256Hash.hashTwice(bosHashPrevouts.toByteArray()); + } + + if (!anyoneCanPay && signAll) { + ByteArrayOutputStream bosSequence = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.inputs.size(); ++i) { + uint32ToByteStreamLE(this.inputs.get(i).getSequenceNumber(), bosSequence); + } + hashSequence = Sha256Hash.hashTwice(bosSequence.toByteArray()); + } + + if (signAll) { + ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.outputs.size(); ++i) { + uint64ToByteStreamLE( + BigInteger.valueOf(this.outputs.get(i).getValue().getValue()), + bosHashOutputs + ); + bosHashOutputs.write(new VarInt(this.outputs.get(i).getScriptBytes().length).encode()); + bosHashOutputs.write(this.outputs.get(i).getScriptBytes()); + } + hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray()); + } else if (basicSigHashType == SigHash.SINGLE.value && inputIndex < outputs.size()) { + ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256); + uint64ToByteStreamLE( + BigInteger.valueOf(this.outputs.get(inputIndex).getValue().getValue()), + bosHashOutputs + ); + bosHashOutputs.write(new VarInt(this.outputs.get(inputIndex).getScriptBytes().length).encode()); + bosHashOutputs.write(this.outputs.get(inputIndex).getScriptBytes()); + hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray()); + } + uint32ToByteStreamLE(version, bos); + bos.write(hashPrevouts); + bos.write(hashSequence); + bos.write(inputs.get(inputIndex).getOutpoint().getHash().getReversedBytes()); + uint32ToByteStreamLE(inputs.get(inputIndex).getOutpoint().getIndex(), bos); + bos.write(new VarInt(scriptCode.length).encode()); + bos.write(scriptCode); + uint64ToByteStreamLE(BigInteger.valueOf(prevValue.getValue()), bos); + uint32ToByteStreamLE(inputs.get(inputIndex).getSequenceNumber(), bos); + bos.write(hashOutputs); + uint32ToByteStreamLE(this.lockTime, bos); + uint32ToByteStreamLE(0x000000ff & sigHashType, bos); + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + + return Sha256Hash.twiceOf(bos.toByteArray()); + } + + @Override protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { bitcoinSerializeToStream(stream, true); diff --git a/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java b/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java index d11eec1d4..c3c5cc4bd 100644 --- a/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java +++ b/src/main/java/co/rsk/bitcoinj/core/TransactionWitness.java @@ -1,10 +1,12 @@ package co.rsk.bitcoinj.core; import co.rsk.bitcoinj.crypto.TransactionSignature; +import co.rsk.bitcoinj.script.Script; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class TransactionWitness { static TransactionWitness empty = new TransactionWitness(0); @@ -13,12 +15,22 @@ public static TransactionWitness getEmpty() { return empty; } - private List pushes; + private final List pushes; public TransactionWitness(int pushCount) { pushes = new ArrayList(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); } + public static TransactionWitness of(List pushes) { + return new TransactionWitness(pushes); + } + + private TransactionWitness(List pushes) { + for (byte[] push : pushes) + Objects.requireNonNull(push); + this.pushes = pushes; + } + public byte[] getPush(int i) { return pushes.get(i); } @@ -46,6 +58,38 @@ public static TransactionWitness createWitness(@Nullable final TransactionSignat return witness; } + public static TransactionWitness createWitnessScript(Script witnessScript, List signatures) { + List pushes = new ArrayList<>(signatures.size() + 2); + pushes.add(new byte[] {}); + for (TransactionSignature signature : signatures) { + pushes.add(signature.encodeToBitcoin()); + } + pushes.add(witnessScript.getProgram()); + return TransactionWitness.of(pushes); + } + + public static TransactionWitness createWitnessErpScript(Script witnessScript, List signatures) { + List pushes = new ArrayList<>(signatures.size() + 3); + pushes.add(new byte[] {}); + for (TransactionSignature signature : signatures) { + pushes.add(signature.encodeToBitcoin()); + } + pushes.add(new byte[] {}); // OP_NOTIF argument. If an empty vector is set it will validate against the standard keys, if a 1 is set it will validate against the emergency keys + pushes.add(witnessScript.getProgram()); + return TransactionWitness.of(pushes); + } + + public static TransactionWitness createWitnessErpEmergencyScript(Script witnessScript, List signatures) { + List pushes = new ArrayList<>(signatures.size() + 3); + pushes.add(new byte[] {}); + for (TransactionSignature signature : signatures) { + pushes.add(signature.encodeToBitcoin()); + } + pushes.add(new byte[] {1}); // OP_NOTIF argument. If an empty vector is set it will validate against the standard keys, if a 1 is set it will validate against the emergency keys + pushes.add(witnessScript.getProgram()); + return TransactionWitness.of(pushes); + } + public byte[] getScriptBytes() { if (getPushCount() == 0) return new byte[0]; diff --git a/src/main/java/co/rsk/bitcoinj/core/Utils.java b/src/main/java/co/rsk/bitcoinj/core/Utils.java index bde1d354e..fd7312a5c 100644 --- a/src/main/java/co/rsk/bitcoinj/core/Utils.java +++ b/src/main/java/co/rsk/bitcoinj/core/Utils.java @@ -263,15 +263,19 @@ public static int readUint16BE(byte[] bytes, int offset) { } /** - * Calculates RIPEMD160(SHA256(input)). This is used in Address calculations. + * Hash160 calculates RIPEMD160(SHA256(input)). This is used in Address calculations. */ - public static byte[] sha256hash160(byte[] input) { - byte[] sha256 = Sha256Hash.hash(input); + + public static byte[] hash160(byte[] input) { + return digestRipeMd160(Sha256Hash.hash(input)); + } + + public static byte[] digestRipeMd160(byte[] sha256) { RIPEMD160Digest digest = new RIPEMD160Digest(); digest.update(sha256, 0, sha256.length); - byte[] out = new byte[20]; - digest.doFinal(out, 0); - return out; + byte[] ripemdHash = new byte[20]; + digest.doFinal(ripemdHash, 0); + return ripemdHash; } /** diff --git a/src/main/java/co/rsk/bitcoinj/crypto/DeterministicKey.java b/src/main/java/co/rsk/bitcoinj/crypto/DeterministicKey.java index 74c79b78d..cdec060c1 100644 --- a/src/main/java/co/rsk/bitcoinj/crypto/DeterministicKey.java +++ b/src/main/java/co/rsk/bitcoinj/crypto/DeterministicKey.java @@ -212,7 +212,7 @@ public byte[] getChainCode() { * Returns RIPE-MD160(SHA256(pub key bytes)). */ public byte[] getIdentifier() { - return Utils.sha256hash160(getPubKey()); + return Utils.hash160(getPubKey()); } /** Returns the first 32 bits of the result of {@link #getIdentifier()}. */ diff --git a/src/main/java/co/rsk/bitcoinj/script/P2shErpFederationRedeemScriptParser.java b/src/main/java/co/rsk/bitcoinj/script/P2shErpFederationRedeemScriptParser.java index 2ab47dcca..d31e71b78 100644 --- a/src/main/java/co/rsk/bitcoinj/script/P2shErpFederationRedeemScriptParser.java +++ b/src/main/java/co/rsk/bitcoinj/script/P2shErpFederationRedeemScriptParser.java @@ -1,5 +1,6 @@ package co.rsk.bitcoinj.script; +import co.rsk.bitcoinj.core.Sha256Hash; import co.rsk.bitcoinj.core.Utils; import co.rsk.bitcoinj.core.VerificationException; import org.slf4j.Logger; @@ -84,6 +85,54 @@ public static Script createP2shErpRedeemScript( return erpRedeemScript; } + public static Script createP2shP2wshErpRedeemScript( + Script defaultFederationRedeemScript, + Script erpFederationRedeemScript, + Long csvValue + ) { + byte[] serializedCsvValue = Utils.signedLongToByteArrayLE(csvValue); + + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + Script erpP2shP2wshRedeemScript = scriptBuilder + .op(ScriptOpCodes.OP_NOTIF) + .addChunks(defaultFederationRedeemScript.getChunks()) + .op(ScriptOpCodes.OP_ELSE) + .data(serializedCsvValue) + .op(ScriptOpCodes.OP_CHECKSEQUENCEVERIFY) + .op(ScriptOpCodes.OP_DROP) + .addChunks(erpFederationRedeemScript.getChunks()) + .op(ScriptOpCodes.OP_ENDIF) + .build(); + return erpP2shP2wshRedeemScript; + } + + public static Script createP2shP2wshErpRedeemScriptWithFlyover( + Script defaultFederationRedeemScript, + Script erpFederationRedeemScript, + Sha256Hash derivationPath, + Long csvValue + ) { + byte[] serializedCsvValue = Utils.signedLongToByteArrayLE(csvValue); + + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + Script erpP2shP2wshRedeemScript = scriptBuilder + .data(derivationPath.getBytes()) + .op(ScriptOpCodes.OP_DROP) + .op(ScriptOpCodes.OP_NOTIF) + .addChunks(defaultFederationRedeemScript.getChunks()) + .op(ScriptOpCodes.OP_ELSE) + .data(serializedCsvValue) + .op(ScriptOpCodes.OP_CHECKSEQUENCEVERIFY) + .op(ScriptOpCodes.OP_DROP) + .addChunks(erpFederationRedeemScript.getChunks()) + .op(ScriptOpCodes.OP_ENDIF) + .build(); + + return erpP2shP2wshRedeemScript; + } + public static boolean isP2shErpFed(List chunks) { return RedeemScriptValidator.hasP2shErpRedeemScriptStructure(chunks); } diff --git a/src/main/java/co/rsk/bitcoinj/script/Script.java b/src/main/java/co/rsk/bitcoinj/script/Script.java index 1cea8b02f..dffab60cc 100644 --- a/src/main/java/co/rsk/bitcoinj/script/Script.java +++ b/src/main/java/co/rsk/bitcoinj/script/Script.java @@ -325,7 +325,7 @@ public BigInteger getCLTVPaymentChannelExpiry() { */ @Deprecated public Address getFromAddress(NetworkParameters params) throws ScriptException { - return new Address(params, Utils.sha256hash160(getPubKey())); + return new Address(params, Utils.hash160(getPubKey())); } /** @@ -1263,7 +1263,7 @@ public static void executeScript(@Nullable BtcTransaction txContainingThis, long case OP_HASH160: if (stack.size() < 1) throw new ScriptException("Attempted OP_HASH160 on an empty stack"); - stack.add(Utils.sha256hash160(stack.pollLast())); + stack.add(Utils.hash160(stack.pollLast())); break; case OP_HASH256: if (stack.size() < 1) diff --git a/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java b/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java index ed69f8948..120516f29 100644 --- a/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java +++ b/src/main/java/co/rsk/bitcoinj/script/ScriptBuilder.java @@ -17,6 +17,7 @@ package co.rsk.bitcoinj.script; import co.rsk.bitcoinj.core.BtcTransaction; +import co.rsk.bitcoinj.core.Sha256Hash; import com.google.common.collect.Lists; import co.rsk.bitcoinj.core.Address; import co.rsk.bitcoinj.core.BtcECKey; @@ -270,13 +271,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() <= 16); // That's the max we can represent with a single opcode. 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(); } @@ -411,10 +412,23 @@ public static Script createP2SHOutputScript(byte[] hash) { * Creates a scriptPubKey for the given redeem script. */ public static Script createP2SHOutputScript(Script redeemScript) { - byte[] hash = Utils.sha256hash160(redeemScript.getProgram()); + byte[] hash = Utils.hash160(redeemScript.getProgram()); return ScriptBuilder.createP2SHOutputScript(hash); } + /** + * Creates a P2SH-P2WSH scriptPubKey for the given redeem script. + */ + public static Script createP2SHP2WSHOutputScript(Script redeemScript) { + byte[] redeemScriptHash = Sha256Hash.hash(redeemScript.getProgram()); + Script witnessScript = new ScriptBuilder() + .number(ScriptOpCodes.OP_0) + .data(redeemScriptHash) + .build(); + + return ScriptBuilder.createP2SHOutputScript(witnessScript); + } + /** * Creates a P2SH output script with given public keys and threshold. Given public keys will be placed in * redeem script in the lexicographical sorting order. diff --git a/src/test/java/co/rsk/bitcoinj/core/UtilsTest.java b/src/test/java/co/rsk/bitcoinj/core/UtilsTest.java index 0c1fece22..c583ba1da 100644 --- a/src/test/java/co/rsk/bitcoinj/core/UtilsTest.java +++ b/src/test/java/co/rsk/bitcoinj/core/UtilsTest.java @@ -18,7 +18,9 @@ package co.rsk.bitcoinj.core; import java.math.BigInteger; +import java.util.Arrays; import java.util.Date; +import java.util.List; import org.junit.Test; import org.spongycastle.util.encoders.Hex; @@ -230,5 +232,34 @@ public void signedLongToByteArrayLE() { reversedConversion = Utils.reverseBytes(conversion); // Turn into BE obtainedValue = new BigInteger(reversedConversion).longValue(); assertEquals(value, obtainedValue); - } + }; + + @Test + public void hash160() { + + List inputs = Arrays.asList( + Hex.decode(""), + Hex.decode("abcd"), + Hex.decode("00"), + Hex.decode("01"), + Hex.decode("0000"), + Hex.decode("ffff") + ); + List expectedHashes = Arrays.asList( + Hex.decode("b472a266d0bd89c13706a4132ccfb16f7c3b9fcb"), + Hex.decode("4671c47a9d20c240a291661520d4af51df08fb0b"), + Hex.decode("9f7fd096d37ed2c0e3f7f0cfc924beef4ffceb68"), + Hex.decode("c51b66bced5e4491001bd702669770dccf440982"), + Hex.decode("e6c41bcc570872e88e58db7c940dc8d399e72aef"), + Hex.decode("e6abebacc6bf964f5131e80b241e3fe14bc3e156") + ); + + for (int i = 0; i < inputs.size(); i++) { + byte[] input = inputs.get(i); + byte[] expectedHash = expectedHashes.get(i); + + byte[] hashResult = Utils.hash160(input); + assertArrayEquals(expectedHash, hashResult); + } + }; } diff --git a/src/test/java/co/rsk/bitcoinj/script/P2shP2wshScriptTest.java b/src/test/java/co/rsk/bitcoinj/script/P2shP2wshScriptTest.java new file mode 100644 index 000000000..5510e0d11 --- /dev/null +++ b/src/test/java/co/rsk/bitcoinj/script/P2shP2wshScriptTest.java @@ -0,0 +1,38 @@ +package co.rsk.bitcoinj.script; + +import co.rsk.bitcoinj.core.Address; +import co.rsk.bitcoinj.core.BtcECKey; +import co.rsk.bitcoinj.core.NetworkParameters; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; +import org.spongycastle.util.encoders.Hex; + +public class P2shP2wshScriptTest { + + @Test + public void getAddressFromP2shP2wshScript() { + List keys = Arrays.asList(new String[]{ + "027de2af71862e0c64bf0ec5a66e3abc3b01fc57877802e6a6a81f6ea1d3561007", + "02d9c67fef9f8d0707cbcca195eb5f26c6a65da6ca2d6130645c434bb924063856", + "0346f033b8652a17d319d3ecbbbf20fd2cd663a6548173b9419d8228eef095012e" + }).stream().map(k -> BtcECKey.fromPublicOnly(Hex.decode(k))).collect(Collectors.toList()); + + Script redeemScript = new ScriptBuilder().createRedeemScript( + keys.size() / 2 + 1, + keys + ); + + Script p2shP2wshOutputScript = ScriptBuilder.createP2SHP2WSHOutputScript(redeemScript); + Address segwitAddress = Address.fromP2SHScript( + NetworkParameters.fromID(NetworkParameters.ID_TESTNET), + p2shP2wshOutputScript + ); + + Assert.assertEquals("2NCQHJJuG2iQjN2Be3QYXwWvFgS6AZ4MEkL", segwitAddress.toBase58()); + // https://mempool.space/testnet/tx/1744459aeaf7369aadc9fc40de9ab2bf575b14e35029b35a7ee4bbd3de65af7f + } + +} diff --git a/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java b/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java index b5bf8f29e..070f29954 100644 --- a/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java +++ b/src/test/java/co/rsk/bitcoinj/script/ScriptTest.java @@ -107,7 +107,7 @@ public void testScriptSig() { byte[] sigProgBytes = Hex.decode(sigProg); Script script = new Script(sigProgBytes); // Test we can extract the from address. - byte[] hash160 = Utils.sha256hash160(script.getPubKey()); + byte[] hash160 = Utils.hash160(script.getPubKey()); Address a = new Address(PARAMS, hash160); assertEquals("mkFQohBpy2HDXrCwyMrYL5RtfrmeiuuPY2", a.toString()); }