Script: migrate two native constructors into a parse() static constructor

This commit is contained in:
Andreas Schildbach 2023-04-05 14:48:40 +02:00
parent 87bf6c0266
commit 28af827c0e
10 changed files with 110 additions and 106 deletions

View File

@ -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]);
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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<ScriptChunk> 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<ScriptChunk> 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<ScriptChunk> 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),
};
/**
* <p>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.</p>
*
* <p>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.</p>
*/
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);
}
}
/**
* <p>If the program somehow pays to a hash, returns the hash.</p>
@ -595,7 +598,7 @@ public class Script {
List<ScriptChunk> 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<ScriptChunk> 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<ScriptChunk> 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);

View File

@ -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");

View File

@ -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)));
}

View File

@ -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());
}

View File

@ -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());

View File

@ -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<VerifyFlag> 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;

View File

@ -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));