diff --git a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java index ad930cc36..338ad232a 100644 --- a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java @@ -172,9 +172,9 @@ public class FullPrunedBlockChain extends AbstractBlockChain { */ private Script getScript(byte[] scriptBytes) { try { - return new Script(scriptBytes); + return Script.parse(scriptBytes); } catch (Exception e) { - return new Script(new byte[0]); + return Script.parse(new byte[0]); } } diff --git a/core/src/main/java/org/bitcoinj/core/TransactionInput.java b/core/src/main/java/org/bitcoinj/core/TransactionInput.java index 5d9af0dbd..b769b2cfb 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionInput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionInput.java @@ -225,7 +225,7 @@ public class TransactionInput { // parameter is overloaded to be something totally different. Script script = scriptSig == null ? null : scriptSig.get(); if (script == null) { - script = new Script(scriptBytes); + script = Script.parse(scriptBytes); scriptSig = new WeakReference<>(script); } return script; diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java index f24cce307..ffacf33a1 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java @@ -121,7 +121,7 @@ public class TransactionOutput { public Script getScriptPubKey() throws ScriptException { if (scriptPubKey == null) { - scriptPubKey = new Script(scriptBytes); + scriptPubKey = Script.parse(scriptBytes); } return scriptPubKey; } diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index 191ecf94f..ad5dfa5e9 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -229,9 +229,85 @@ public class Script { // Creation time of the associated keys, or null if unknown. @Nullable private Instant creationTime; - /** Creates an empty script that serializes to nothing. */ - private Script() { - chunks = new ArrayList<>(); + /** + * Construct a script that copies and wraps a given program. The array is parsed and checked for syntactic + * validity. Programs like this are e.g. used in {@link TransactionInput} and {@link TransactionOutput}. + * + * @param program array of program bytes + * @return parsed program + * @throws ScriptException if the program could not be parsed + */ + public static Script parse(byte[] program) throws ScriptException { + return parse(program, TimeUtils.currentTime()); + } + + /** + * Construct a script that copies and wraps a given program, and a creation time. The array is parsed and checked + * for syntactic validity. Programs like this are e.g. used in {@link TransactionInput} and + * {@link TransactionOutput}. + * + * @param program Array of program bytes from a transaction. + * @param creationTime creation time of the script + * @return parsed program + * @throws ScriptException if the program could not be parsed + */ + public static Script parse(byte[] program, Instant creationTime) throws ScriptException { + Objects.requireNonNull(creationTime); + program = Arrays.copyOf(program, program.length); // defensive copy + List chunks = new ArrayList<>(5); // common size + parseIntoChunks(program, chunks); + return new Script(program, chunks, creationTime); + } + + /** + * To run a script, first we parse it which breaks it up into chunks representing pushes of data or logical + * opcodes. Then we can run the parsed chunks. + */ + private static void parseIntoChunks(byte[] program, List chunks) throws ScriptException { + ByteArrayInputStream bis = new ByteArrayInputStream(program); + while (bis.available() > 0) { + int opcode = bis.read(); + + long dataToRead = -1; + if (opcode >= 0 && opcode < OP_PUSHDATA1) { + // Read some bytes of data, where how many is the opcode value itself. + dataToRead = opcode; + } else if (opcode == OP_PUSHDATA1) { + if (bis.available() < 1) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); + dataToRead = bis.read(); + } else if (opcode == OP_PUSHDATA2) { + // Read a short, then read that many bytes of data. + if (bis.available() < 2) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); + dataToRead = ByteUtils.readUint16(bis); + } else if (opcode == OP_PUSHDATA4) { + // Read a uint32, then read that many bytes of data. + // Though this is allowed, because its value cannot be > 520, it should never actually be used + if (bis.available() < 4) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); + dataToRead = ByteUtils.readUint32(bis); + } + + ScriptChunk chunk; + if (dataToRead == -1) { + chunk = new ScriptChunk(opcode, null); + } else { + if (dataToRead > bis.available()) + throw new ScriptException(ScriptError.SCRIPT_ERR_BAD_OPCODE, "Push of data element that is larger than remaining data: " + dataToRead + " vs " + bis.available()); + byte[] data = new byte[(int)dataToRead]; + checkState(dataToRead == 0 || bis.read(data, 0, (int) dataToRead) == dataToRead); + chunk = new ScriptChunk(opcode, data); + } + // Save some memory by eliminating redundant copies of the same chunk objects. + for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) { + if (c.equals(chunk)) chunk = c; + } + chunks.add(chunk); + } + } + + private Script(byte[] programBytes, List chunks, Instant creationTime) { + this.program = programBytes; + this.chunks = chunks; + this.creationTime = creationTime; } // Used from ScriptBuilder. @@ -240,29 +316,6 @@ public class Script { this.creationTime = creationTime; } - /** - * Construct a Script that copies and wraps the programBytes array. The array is parsed and checked for syntactic - * validity. Use this if the creation time is not known. - * @param programBytes Array of program bytes from a transaction. - */ - public Script(byte[] programBytes) throws ScriptException { - this.program = programBytes; - parse(programBytes); - this.creationTime = null; - } - - /** - * Construct a Script that copies and wraps the programBytes array. The array is parsed and checked for syntactic - * validity. - * @param programBytes Array of program bytes from a transaction. - * @param creationTime creation time of the script - */ - public Script(byte[] programBytes, Instant creationTime) throws ScriptException { - this.program = programBytes; - parse(programBytes); - this.creationTime = Objects.requireNonNull(creationTime); - } - /** * Gets the creation time of this script, or empty if unknown. * @return creation time of this script, or empty if unknown @@ -344,56 +397,6 @@ public class Script { new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null), }; - /** - *

To run a script, first we parse it which breaks it up into chunks representing pushes of data or logical - * opcodes. Then we can run the parsed chunks.

- * - *

The reason for this split, instead of just interpreting directly, is to make it easier - * to reach into a programs structure and pull out bits of data without having to run it. - * This is necessary to render the to addresses of transactions in a user interface. - * Bitcoin Core does something similar.

- */ - private void parse(byte[] program) throws ScriptException { - chunks = new ArrayList<>(5); // Common size. - ByteArrayInputStream bis = new ByteArrayInputStream(program); - while (bis.available() > 0) { - int opcode = bis.read(); - - long dataToRead = -1; - if (opcode >= 0 && opcode < OP_PUSHDATA1) { - // Read some bytes of data, where how many is the opcode value itself. - dataToRead = opcode; - } else if (opcode == OP_PUSHDATA1) { - if (bis.available() < 1) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); - dataToRead = bis.read(); - } else if (opcode == OP_PUSHDATA2) { - // Read a short, then read that many bytes of data. - if (bis.available() < 2) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); - dataToRead = ByteUtils.readUint16(bis); - } else if (opcode == OP_PUSHDATA4) { - // Read a uint32, then read that many bytes of data. - // Though this is allowed, because its value cannot be > 520, it should never actually be used - if (bis.available() < 4) throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unexpected end of script"); - dataToRead = ByteUtils.readUint32(bis); - } - - ScriptChunk chunk; - if (dataToRead == -1) { - chunk = new ScriptChunk(opcode, null); - } else { - if (dataToRead > bis.available()) - throw new ScriptException(ScriptError.SCRIPT_ERR_BAD_OPCODE, "Push of data element that is larger than remaining data: " + dataToRead + " vs " + bis.available()); - byte[] data = new byte[(int)dataToRead]; - checkState(dataToRead == 0 || bis.read(data, 0, (int) dataToRead) == dataToRead); - chunk = new ScriptChunk(opcode, data); - } - // Save some memory by eliminating redundant copies of the same chunk objects. - for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) { - if (c.equals(chunk)) chunk = c; - } - chunks.add(chunk); - } - } /** *

If the program somehow pays to a hash, returns the hash.

@@ -595,7 +598,7 @@ public class Script { List existingChunks = chunks.subList(1, chunks.size() - 1); ScriptChunk redeemScriptChunk = chunks.get(chunks.size() - 1); Objects.requireNonNull(redeemScriptChunk.data); - Script redeemScript = new Script(redeemScriptChunk.data); + Script redeemScript = Script.parse(redeemScriptChunk.data); int sigCount = 0; int myIndex = redeemScript.findKeyInRedeem(signingKey); @@ -713,31 +716,32 @@ public class Script { * Gets the count of regular SigOps in the script program (counting multisig ops as 20) */ public static int getSigOpCount(byte[] program) throws ScriptException { - Script script = new Script(); + List chunks = new ArrayList<>(5); // common size try { - script.parse(program); + parseIntoChunks(program, chunks); } catch (ScriptException e) { // Ignore errors and count up to the parse-able length } - return getSigOpCount(script.chunks, false); + return getSigOpCount(chunks, false); } - + /** * Gets the count of P2SH Sig Ops in the Script scriptSig */ public static long getP2SHSigOpCount(byte[] scriptSig) throws ScriptException { - Script script = new Script(); + List chunks = new ArrayList<>(5); // common size try { - script.parse(scriptSig); + parseIntoChunks(scriptSig, chunks); } catch (ScriptException e) { // Ignore errors and count up to the parse-able length } - for (int i = script.chunks.size() - 1; i >= 0; i--) - if (!script.chunks.get(i).isOpCode()) { - Script subScript = new Script(); - subScript.parse(script.chunks.get(i).data); + Collections.reverse(chunks); + for (ScriptChunk chunk : chunks) { + if (!chunk.isOpCode()) { + Script subScript = parse(chunk.data); return getSigOpCount(subScript.chunks, true); } + } return 0; } @@ -1786,7 +1790,7 @@ public class Script { throw new ScriptException(ScriptError.SCRIPT_ERR_SIG_PUSHONLY, "Attempted to spend a P2SH scriptPubKey with a script that contained the script op " + chunk); byte[] scriptPubKeyBytes = p2shStack.pollLast(); - Script scriptPubKeyP2SH = new Script(scriptPubKeyBytes); + Script scriptPubKeyP2SH = Script.parse(scriptPubKeyBytes); executeScript(txContainingThis, scriptSigIndex, scriptPubKeyP2SH, p2shStack, verifyFlags); diff --git a/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java b/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java index 4115c218f..1742508c2 100644 --- a/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java +++ b/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java @@ -507,9 +507,9 @@ public class WalletProtobufSerializer { byte[] programBytes = protoScript.getProgram().toByteArray(); Script script; if (creationTimestamp > 0) - script = new Script(programBytes, Instant.ofEpochMilli(creationTimestamp)); + script = Script.parse(programBytes, Instant.ofEpochMilli(creationTimestamp)); else - script = new Script(programBytes); + script = Script.parse(programBytes); scripts.add(script); } catch (ScriptException e) { throw new UnreadableWalletException("Unparseable script in wallet"); diff --git a/core/src/test/java/org/bitcoinj/base/SegwitAddressTest.java b/core/src/test/java/org/bitcoinj/base/SegwitAddressTest.java index 095f0a067..043fa9f34 100644 --- a/core/src/test/java/org/bitcoinj/base/SegwitAddressTest.java +++ b/core/src/test/java/org/bitcoinj/base/SegwitAddressTest.java @@ -152,7 +152,7 @@ public class SegwitAddressTest { ByteUtils.formatHex(ScriptBuilder.createOutputScript(address).getProgram())); assertEquals(valid.address.toLowerCase(Locale.ROOT), address.toBech32()); if (valid.expectedWitnessVersion == 0) { - Script expectedScriptPubKey = new Script(ByteUtils.parseHex(valid.expectedScriptPubKey)); + Script expectedScriptPubKey = Script.parse(ByteUtils.parseHex(valid.expectedScriptPubKey)); assertEquals(address, SegwitAddress.fromHash(valid.expectedNetwork, ScriptPattern.extractHashFromP2WH(expectedScriptPubKey))); } diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index cdfa3f53d..4b5925857 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -451,7 +451,7 @@ public class TransactionTest { ECKey pubKey = ECKey.fromPublicOnly(ByteUtils.parseHex( "02d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b")); - Script script = new Script(ByteUtils.parseHex( + Script script = Script.parse(ByteUtils.parseHex( "56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); Sha256Hash hash = tx.hashForWitnessSignature(0, script, Coin.valueOf(987654321L), Transaction.SigHash.SINGLE, true); @@ -570,11 +570,11 @@ public class TransactionTest { int size1 = tx1.getMessageSize(); int size2 = tx1.getMessageSizeForPriorityCalc(); assertEquals(113, size1 - size2); - tx1.getInput(0).setScriptSig(new Script(new byte[109])); + tx1.getInput(0).setScriptSig(Script.parse(new byte[109])); assertEquals(78, tx1.getMessageSizeForPriorityCalc()); - tx1.getInput(0).setScriptSig(new Script(new byte[110])); + tx1.getInput(0).setScriptSig(Script.parse(new byte[110])); assertEquals(78, tx1.getMessageSizeForPriorityCalc()); - tx1.getInput(0).setScriptSig(new Script(new byte[111])); + tx1.getInput(0).setScriptSig(Script.parse(new byte[111])); assertEquals(79, tx1.getMessageSizeForPriorityCalc()); } diff --git a/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java b/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java index 3acc626de..50d399eff 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java @@ -60,7 +60,7 @@ public class TransactionWitnessTest { ECKey.ECDSASignature ecdsaSignature2 = TransactionSignature.decodeFromDER(ByteUtils.parseHex("3045022100fcfe4a58f2878047ef7c5889fc52a3816ad2dd218807daa3c3eafd4841ffac4d022073454df7e212742f0fee20416b418a2c1340a33eebed5583d19a61088b112832")); TransactionSignature signature2 = new TransactionSignature(ecdsaSignature2, Transaction.SigHash.ALL, false); - Script witnessScript = new Script(ByteUtils.parseHex("522102bb65b325a986c5b15bd75e0d81cf149219597617a70995efedec6309b4600fa02103c54f073f5db9f68915019801435058c9232cb72c6528a2ca15af48eb74ca8b9a52ae")); + Script witnessScript = Script.parse(ByteUtils.parseHex("522102bb65b325a986c5b15bd75e0d81cf149219597617a70995efedec6309b4600fa02103c54f073f5db9f68915019801435058c9232cb72c6528a2ca15af48eb74ca8b9a52ae")); TransactionWitness witness = TransactionWitness.redeemP2WSH(witnessScript, signature1, signature2); assertEquals(4, witness.getPushCount()); diff --git a/core/src/test/java/org/bitcoinj/script/ScriptTest.java b/core/src/test/java/org/bitcoinj/script/ScriptTest.java index 33019aeee..c496f12f9 100644 --- a/core/src/test/java/org/bitcoinj/script/ScriptTest.java +++ b/core/src/test/java/org/bitcoinj/script/ScriptTest.java @@ -91,7 +91,7 @@ public class ScriptTest { @Test public void testScriptSig() { byte[] sigProgBytes = ByteUtils.parseHex(sigProg); - Script script = new Script(sigProgBytes); + Script script = Script.parse(sigProgBytes); assertEquals( "PUSHDATA(71)[304402202b4da291cc39faf8433911988f9f49fc5c995812ca2f94db61468839c228c3e90220628bff3ff32ec95825092fa051cba28558a981fcf59ce184b14f2e215e69106701] PUSHDATA(65)[0414b38f4be3bb9fa0f4f32b74af07152b2f2f630bc02122a491137b6c523e46f18a0d5034418966f93dfc37cc3739ef7b2007213a302b7fba161557f4ad644a1c]", script.toString()); @@ -101,7 +101,7 @@ public class ScriptTest { public void testScriptPubKey() { // Check we can extract the to address byte[] pubkeyBytes = ByteUtils.parseHex(pubkeyProg); - Script pubkey = new Script(pubkeyBytes); + Script pubkey = Script.parse(pubkeyBytes); assertEquals("DUP HASH160 PUSHDATA(20)[33e81a941e64cda12c6a299ed322ddbdd03f8d0e] EQUALVERIFY CHECKSIG", pubkey.toString()); Address toAddr = LegacyAddress.fromPubKeyHash(BitcoinNetwork.TESTNET, ScriptPattern.extractHashFromP2PKH(pubkey)); assertEquals("mkFQohBpy2HDXrCwyMrYL5RtfrmeiuuPY2", toAddr.toString()); @@ -142,7 +142,7 @@ public class ScriptTest { @Test public void testIp() { byte[] bytes = ByteUtils.parseHex("41043e96222332ea7848323c08116dddafbfa917b8e37f0bdf63841628267148588a09a43540942d58d49717ad3fabfe14978cf4f0a8b84d2435dad16e9aa4d7f935ac"); - Script s = new Script(bytes); + Script s = Script.parse(bytes); assertTrue(ScriptPattern.isP2PK(s)); } @@ -289,7 +289,7 @@ public class ScriptTest { } } - return new Script(out.toByteArray()); + return Script.parse(out.toByteArray()); } private Set parseVerifyFlags(String str) { @@ -375,7 +375,7 @@ public class ScriptTest { tx.addInput(txInput); TransactionOutput txOutput = new TransactionOutput(tx, creditingTransaction.getOutput(0).getValue(), - new Script(new byte[] {}).getProgram()); + Script.parse(new byte[] {}).getProgram()); tx.addOutput(txOutput); return tx; diff --git a/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java b/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java index 84da924de..654178eef 100644 --- a/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/DefaultRiskAnalysisTest.java @@ -208,7 +208,7 @@ public class DefaultRiskAnalysisTest { "010000000200a2be4376b7f47250ad9ad3a83b6aa5eb6a6d139a1f50771704d77aeb8ce76c010000006a4730440220055723d363cd2d4fe4e887270ebdf5c4b99eaf233a5c09f9404f888ec8b839350220763c3794d310b384ce86decfb05787e5bfa5d31983db612a2dde5ffec7f396ae012102ef47e27e0c4bdd6dc83915f185d972d5eb8515c34d17bad584a9312e59f4e0bcffffffff52239451d37757eeacb86d32864ec1ee6b6e131d1e3fee6f1cff512703b71014030000006c493046022100ea266ac4f893d98a623a6fc0e6a961cd5a3f32696721e87e7570a68851917e75022100a928a3c4898be60909347e765f52872a613d8aada66c57a8c8791316d2f298710121038bb455ca101ebbb0ecf7f5c01fa1dcb7d14fbf6b7d7ea52ee56f0148e72a736cffffffff0630b15a00000000001976a9146ae477b690cf85f21c2c01e2c8639a5c18dc884e88ac4f260d00000000001976a91498d08c02ab92a671590adb726dddb719695ee12e88ac65753b00000000001976a9140b2eb4ba6d364c82092f25775f56bc10cd92c8f188ac65753b00000000001976a914d1cb414e22081c6ba3a935635c0f1d837d3c5d9188ac65753b00000000001976a914df9d137a0d279471a2796291874c29759071340b88ac3d753b00000000001976a91459f5aa4815e3aa8e1720e8b82f4ac8e6e904e47d88ac00000000"))); assertEquals("dbe4147cf89b89fd9fa6c8ce6a3e2adecb234db094ec88301ae09073ca17d61d", tx2.getTxId().toString()); assertFalse(ECKey.ECDSASignature - .decodeFromDER(new Script(tx2.getInput(1).getScriptBytes()).getChunks().get(0).data) + .decodeFromDER(Script.parse(tx2.getInput(1).getScriptBytes()).getChunks().get(0).data) .isCanonical()); assertEquals(RuleViolation.SIGNATURE_CANONICAL_ENCODING, DefaultRiskAnalysis.isStandard(tx2));