diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java index a2241c3416..034b74d0ce 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java @@ -2465,34 +2465,35 @@ public void registerBtcCoinbaseTransaction( Sha256Hash witnessMerkleRoot, byte[] witnessReservedValue ) throws VMException { + final String LOG_PREFIX = "[registerBtcCoinbaseTransaction]"; Context.propagate(btcContext); try{ this.ensureBtcBlockStore(); }catch (BlockStoreException | IOException e) { String message = String.format("Exception in registerBtcCoinbaseTransaction. %s", e.getMessage()); - logger.warn("[registerBtcCoinbaseTransaction] {}", message); + logger.warn("{} {}", LOG_PREFIX, message); throw new VMException(message, e); } Sha256Hash btcTxHash = BtcTransactionFormatUtils.calculateBtcTxHash(btcTxSerialized); - logger.debug("[registerBtcCoinbaseTransaction] Going to register coinbase information for btcTx: {}", btcTxHash); + logger.debug("{} Going to register coinbase information for btcTx: {}", LOG_PREFIX, btcTxHash); if (witnessReservedValue.length != 32) { String message = String.format( "Witness reserved value length can't be different than 32 bytes. Value received: %s", Bytes.of(witnessReservedValue) ); - logger.warn("[registerBtcCoinbaseTransaction] {}", message); + logger.warn("{} {}", LOG_PREFIX, message); throw new BridgeIllegalArgumentException(message); } - logger.trace("[registerBtcCoinbaseTransaction] Witness reserved value: {}", Bytes.of(witnessReservedValue)); + logger.trace("{} Witness reserved value: {}", LOG_PREFIX, Bytes.of(witnessReservedValue)); if (!PartialMerkleTreeFormatUtils.hasExpectedSize(pmtSerialized)) { String message = String.format( "PartialMerkleTree doesn't have expected size. Value received: %s", Bytes.of(pmtSerialized) ); - logger.warn("[registerBtcCoinbaseTransaction] {}", message); + logger.warn("{} {}", LOG_PREFIX, message); throw new BridgeIllegalArgumentException(message); } @@ -2503,7 +2504,8 @@ public void registerBtcCoinbaseTransaction( merkleRoot = pmt.getTxnHashAndMerkleRoot(hashesInPmt); if (!hashesInPmt.contains(btcTxHash)) { logger.warn( - "[registerBtcCoinbaseTransaction] Supplied btc tx {} is not in the supplied partial merkle tree {}", + "{} Supplied btc tx {} is not in the supplied partial merkle tree {}", + LOG_PREFIX, btcTxHash, pmt ); @@ -2511,10 +2513,10 @@ public void registerBtcCoinbaseTransaction( } } catch (VerificationException e) { String message = String.format("Partial merkle tree could not be parsed. %s", Bytes.of(pmtSerialized)); - logger.warn("[registerBtcCoinbaseTransaction] {}", message); + logger.warn("{} {}", LOG_PREFIX, message); throw new BridgeIllegalArgumentException(message, e); } - logger.trace("[registerBtcCoinbaseTransaction] Merkle root: {}", merkleRoot); + logger.trace("{} Merkle root: {}", LOG_PREFIX, merkleRoot); // Check merkle root equals btc block merkle root at the specified height in the btc best chain // Btc blockstore is available since we've already queried the best chain height @@ -2523,7 +2525,8 @@ public void registerBtcCoinbaseTransaction( storedBlock = btcBlockStore.get(blockHash); } catch (BlockStoreException e) { logger.error( - "[registerBtcCoinbaseTransaction] Error gettin block {} from block store. {}", + "{} Error gettin block {} from block store. {}", + LOG_PREFIX, blockHash, e.getMessage() ); @@ -2531,11 +2534,12 @@ public void registerBtcCoinbaseTransaction( if (storedBlock == null) { String message = String.format("Block %s not yet registered", blockHash); - logger.warn("[registerBtcCoinbaseTransaction] {}", message); + logger.warn("{} {}", LOG_PREFIX, message); throw new BridgeIllegalArgumentException(message); } logger.trace( - "[registerBtcCoinbaseTransaction] Found block with hash {} at height {}", + "{} Found block with hash {} at height {}", + LOG_PREFIX, blockHash, storedBlock.getHeight() ); @@ -2548,8 +2552,7 @@ public void registerBtcCoinbaseTransaction( merkleRoot, blockHeader.getMerkleRoot() ); - logger.warn("[registerBtcCoinbaseTransaction] {}", panicMessage); - panicProcessor.panic("btclock", panicMessage); + logger.warn("{} {}", LOG_PREFIX, panicMessage); return; } @@ -2561,7 +2564,7 @@ public void registerBtcCoinbaseTransaction( CoinbaseInformation coinbaseInformation = new CoinbaseInformation(witnessMerkleRoot); provider.setCoinbaseInformation(blockHeader.getHash(), coinbaseInformation); - logger.debug("[registerBtcCoinbaseTransaction] Registered coinbase information for btc tx {}", btcTxHash); + logger.debug("{} Registered coinbase information for btc tx {}", LOG_PREFIX, btcTxHash); } private void validateWitnessInformation( @@ -2569,7 +2572,7 @@ private void validateWitnessInformation( Sha256Hash witnessMerkleRoot, byte[] witnessReservedValue ) throws BridgeIllegalArgumentException { - Optional expectedWitnessCommitment = findWitnessCommitment(coinbaseTransaction); + Optional expectedWitnessCommitment = findWitnessCommitment(coinbaseTransaction, activations); Sha256Hash calculatedWitnessCommitment = Sha256Hash.twiceOf(witnessMerkleRoot.getReversedBytes(), witnessReservedValue); if (expectedWitnessCommitment.isEmpty() || !expectedWitnessCommitment.get().equals(calculatedWitnessCommitment)) { @@ -3188,7 +3191,6 @@ protected boolean validationsForRegisterBtcTransaction(Sha256Hash btcTxHash, int } catch (Exception e) { String panicMessage = String.format("[validationsForRegisterBtcTransaction] Btc Tx %s Supplied Height is %d but should be greater than 0", btcTxHash, height); logger.warn(panicMessage); - panicProcessor.panic("btclock", panicMessage); return false; } @@ -3227,7 +3229,6 @@ protected boolean validationsForRegisterBtcTransaction(Sha256Hash btcTxHash, int blockHeader.getMerkleRoot() ); logger.warn(panicMessage); - panicProcessor.panic("btclock", panicMessage); return false; } diff --git a/rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java b/rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java index 05a109b92a..765c9ffd5a 100644 --- a/rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java +++ b/rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java @@ -8,13 +8,15 @@ import com.google.common.collect.Lists; import java.util.*; import org.bouncycastle.util.encoders.Hex; +import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BitcoinUtils { protected static final byte[] WITNESS_COMMITMENT_HEADER = Hex.decode("aa21a9ed"); protected static final int WITNESS_COMMITMENT_LENGTH = WITNESS_COMMITMENT_HEADER.length + Sha256Hash.LENGTH; - private static final int MINIMUM_WITNESS_COMMITMENT_SIZE = WITNESS_COMMITMENT_LENGTH + 2; // 1 extra by for OP_RETURN and another one for data length + private static final int MINIMUM_WITNESS_COMMITMENT_SIZE = WITNESS_COMMITMENT_LENGTH + 2; // 1 extra byte for OP_RETURN and another one for data length private static final Logger logger = LoggerFactory.getLogger(BitcoinUtils.class); private static final int FIRST_INPUT_INDEX = 0; @@ -110,15 +112,16 @@ public static Sha256Hash generateSigHashForP2SHTransactionInput(BtcTransaction b .orElseThrow(() -> new IllegalArgumentException("Couldn't extract redeem script from p2sh input")); } - public static Optional findWitnessCommitment(BtcTransaction tx) { + public static Optional findWitnessCommitment(BtcTransaction tx, ActivationConfig.ForBlock activations) { Preconditions.checkState(tx.isCoinBase()); // If more than one witness commitment, take the last one as the valid one List outputsReversed = Lists.reverse(tx.getOutputs()); for (TransactionOutput output : outputsReversed) { - Script scriptPubKey = output.getScriptPubKey(); - if (isWitnessCommitment(scriptPubKey)) { - Sha256Hash witnessCommitment = extractWitnessCommitmentHash(scriptPubKey); + byte[] scriptPubKeyBytes = getOutputScriptPubKeyBytes(activations, output); + + if (isWitnessCommitment(scriptPubKeyBytes)) { + Sha256Hash witnessCommitment = extractWitnessCommitmentHash(scriptPubKeyBytes); return Optional.of(witnessCommitment); } } @@ -126,9 +129,18 @@ public static Optional findWitnessCommitment(BtcTransaction tx) { return Optional.empty(); } - private static boolean isWitnessCommitment(Script scriptPubKey) { - byte[] scriptPubKeyProgram = scriptPubKey.getProgram(); + private static byte[] getOutputScriptPubKeyBytes(ActivationConfig.ForBlock activations, TransactionOutput output) { + if (!activations.isActive(ConsensusRule.RSKIP460)) { + /* + Calling `getScriptPubKey` pre RSKIP460 keeps consensus by throwing a ScripException + when the output has a non-standard format that bitcoinj-thin is not able to parse + */ + return output.getScriptPubKey().getProgram(); + } + return output.getScriptBytes(); + } + private static boolean isWitnessCommitment(byte[] scriptPubKeyProgram) { return scriptPubKeyProgram.length >= MINIMUM_WITNESS_COMMITMENT_SIZE && hasCommitmentStructure(scriptPubKeyProgram); } @@ -146,8 +158,7 @@ private static boolean hasWitnessCommitmentHeader(byte[] header) { /** * Retrieves the hash from a segwit commitment (in an output of the coinbase transaction). */ - private static Sha256Hash extractWitnessCommitmentHash(Script scriptPubKey) { - byte[] scriptPubKeyProgram = scriptPubKey.getProgram(); + private static Sha256Hash extractWitnessCommitmentHash(byte[] scriptPubKeyProgram) { Preconditions.checkState(scriptPubKeyProgram.length >= MINIMUM_WITNESS_COMMITMENT_SIZE); final int WITNESS_COMMITMENT_HASH_START = 6; // 4 bytes for header + OP_RETURN + data length diff --git a/rskj-core/src/main/java/org/ethereum/config/blockchain/upgrades/ConsensusRule.java b/rskj-core/src/main/java/org/ethereum/config/blockchain/upgrades/ConsensusRule.java index 4667e5e546..0569db14a7 100644 --- a/rskj-core/src/main/java/org/ethereum/config/blockchain/upgrades/ConsensusRule.java +++ b/rskj-core/src/main/java/org/ethereum/config/blockchain/upgrades/ConsensusRule.java @@ -105,6 +105,7 @@ public enum ConsensusRule { RSKIP453("rskip453"), RSKIP454("rskip454"), RSKIP459("rskip459"), + RSKIP460("rskip460") ; private final String configKey; diff --git a/rskj-core/src/main/resources/expected.conf b/rskj-core/src/main/resources/expected.conf index 5710410f5e..5cc6dc0586 100644 --- a/rskj-core/src/main/resources/expected.conf +++ b/rskj-core/src/main/resources/expected.conf @@ -106,6 +106,7 @@ blockchain = { rskip453 = rskip454 = rskip459 = + rskip460 = } } gc = { diff --git a/rskj-core/src/main/resources/reference.conf b/rskj-core/src/main/resources/reference.conf index f7f5d4bb8a..63c7db423c 100644 --- a/rskj-core/src/main/resources/reference.conf +++ b/rskj-core/src/main/resources/reference.conf @@ -91,6 +91,7 @@ blockchain = { rskip453 = lovell700 rskip454 = lovell700 rskip459 = lovell700 + rskip460 = lovell700 } } gc = { diff --git a/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinUtilsTest.java b/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinUtilsTest.java index 8261a9027a..be7d871d67 100644 --- a/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinUtilsTest.java +++ b/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinUtilsTest.java @@ -1,6 +1,6 @@ package co.rsk.peg.bitcoin; -import static co.rsk.peg.bitcoin.BitcoinTestUtils.addInputFromMatchingOutputScript; +import static co.rsk.bitcoinj.script.ScriptOpCodes.OP_NOT; import static co.rsk.peg.bitcoin.BitcoinUtils.*; import static org.junit.jupiter.api.Assertions.*; @@ -14,6 +14,8 @@ import java.util.stream.Stream; import co.rsk.peg.federation.StandardMultiSigFederationBuilder; import org.bouncycastle.util.encoders.Hex; +import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.config.blockchain.upgrades.ActivationConfigsForTest; import org.ethereum.util.ByteUtil; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; @@ -21,12 +23,28 @@ import org.junit.jupiter.params.provider.MethodSource; class BitcoinUtilsTest { + + private static final BridgeConstants bridgeTestnetConstants = BridgeMainNetConstants.getInstance(); + private static final NetworkParameters btcTestnetParams = bridgeTestnetConstants.getBtcParams(); + private static final BridgeConstants bridgeMainnetConstants = BridgeMainNetConstants.getInstance(); private static final NetworkParameters btcMainnetParams = bridgeMainnetConstants.getBtcParams(); private static final int FIRST_INPUT_INDEX = 0; + private static final ActivationConfig.ForBlock arrowHeadActivations = ActivationConfigsForTest.arrowhead600().forBlock(0); + private static final ActivationConfig.ForBlock allActivations = ActivationConfigsForTest.all().forBlock(0); + private Address destinationAddress; + // malformed coinbase tx from testnet: https://mempool.space/testnet/tx/ae0a6c774908d3ddd334d40cc385ef1c2ad7e6381a69058114899f5ce567f26c + private static final BtcTransaction malFormedTestnetCoinbaseTx = new BtcTransaction(btcTestnetParams, Hex.decode("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff32030a0e250004fee5346404196a763b0cc3dd196400000000000000000a636b706f6f6c0e2f6d696e65642062792072736b2fffffffff039cce2a00000000001976a914ec2f9ffaba0d68ea6bd7c25cedfe2ae710938e6088ac0000000000000000266a24aa21a9ede46b6d3bc71412e8905cedfad91532bdccb693d93a1335fee0b6223a7ed1ee5b00000000000000002a6a52534b424c4f434b3a8bc552daa25a7a473ac4640ba2b9d621c95652c61488143ca02bbf1b00392fb10120000000000000000000000000000000000000000000000000000000000000000000000000")); + + // malformed coinbase tx from mainnet: https://mempool.space/tx/079e68032d9a4cdb222f464b9966756ccb749633aee678c6a51536b4fc38e29c + private static final BtcTransaction malFormedMainnetCoinbaseTx = new BtcTransaction(btcMainnetParams, Hex.decode("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3c03ebba0c040c93ef652f4d41524120506f6f6c202876303232373234292f76649b3c094f135bf4b83108c14ea85f12307cd4bf00d6010000ffffffffffffffff0371c71a27000000001976a9142fc701e2049ee4957b07134b6c1d771dd5a96b2188ac0000000000000000266a24aa21a9ed5e4aae37309d88e9f49d3a4c6fb424e491cbdbac754b8ef06bb90d2a149bd96900000000000000002cfabe6d6d9797129041127735e99e277241fbc539b327b16ad3e8537125cdc12ccf2d0586010000000000000001200000000000000000000000000000000000000000000000000000000000000000e5a28d27")); + + // witness commitment output script in non-standard format + private static final BtcTransaction coinbaseTxWithWitnessCommitmentOutputInNonStandardFormat = new BtcTransaction(btcMainnetParams, Hex.decode("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff32030a0e250004fee5346404196a763b0cc3dd196400000000000000000a636b706f6f6c0e2f6d696e65642062792072736b2fffffffff039cce2a00000000001976a914ec2f9ffaba0d68ea6bd7c25cedfe2ae710938e6088ac0000000000000000266a24aa21a9ede46b6d3bc71412e8905cedfad91532bdccb693d93a1335fee0b6223a7ed1ee5b00000000000000002a6a24aa21a9ed4f434b3a8bc552daa25a7a473ac4640ba2b9d621c95652c61488143ca02bbf1b00392fb10120000000000000000000000000000000000000000000000000000000000000000000000000")); + @BeforeEach void init() { destinationAddress = BitcoinTestUtils.createP2PKHAddress(btcMainnetParams, "destinationAddress"); @@ -531,17 +549,26 @@ void searchForOutput_whenTheOutputIsNotThere_shouldReturnEmpty() { @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Tag("test find witness commitment implementations") class FindWitnessCommitment { - @Test - void findWitnessCommitment_whenTxHasNoOutputs_shouldThrowException() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_whenTxHasNoOutputs_shouldThrowException(ActivationConfig.ForBlock activations) { // Arrange BtcTransaction btcTx = new BtcTransaction(btcMainnetParams); // Act - assertThrows(IllegalStateException.class, () -> BitcoinUtils.findWitnessCommitment(btcTx)); + assertThrows(IllegalStateException.class, () -> BitcoinUtils.findWitnessCommitment(btcTx, activations)); + } + + private static Stream activationsProvider() { + return Stream.of( + Arguments.of(arrowHeadActivations), + Arguments.of(allActivations) + ); } - @Test - void findWitnessCommitment_whenTxIsNotCoinbase_shouldThrowException() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_whenTxIsNotCoinbase_shouldThrowException(ActivationConfig.ForBlock activations) { // Arrange BtcTransaction btcTx = new BtcTransaction(btcMainnetParams); @@ -555,23 +582,25 @@ void findWitnessCommitment_whenTxIsNotCoinbase_shouldThrowException() { btcTx.addOutput(Coin.COIN, receiver2); // Act - assertThrows(IllegalStateException.class, () -> BitcoinUtils.findWitnessCommitment(btcTx)); + assertThrows(IllegalStateException.class, () -> BitcoinUtils.findWitnessCommitment(btcTx, activations)); } - @Test - void findWitnessCommitment_whenNoWitnessCommitment_shouldReturnEmpty() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_whenNoWitnessCommitment_shouldReturnEmpty(ActivationConfig.ForBlock activations) { // Arrange BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransaction(btcMainnetParams); // Act - Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx); + Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); // Assert assertFalse(witnessCommitment.isPresent()); } - @Test - void findWitnessCommitment_withWitnessCommitment_shouldReturnExpectedValue() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withWitnessCommitment_shouldReturnExpectedValue(ActivationConfig.ForBlock activations) { // Arrange Sha256Hash witnessCommitment = BitcoinTestUtils.createHash(100); BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransactionWithWitnessCommitment( @@ -580,15 +609,16 @@ void findWitnessCommitment_withWitnessCommitment_shouldReturnExpectedValue() { ); // Act - Optional witnessCommitmentFound = BitcoinUtils.findWitnessCommitment(btcTx); + Optional witnessCommitmentFound = BitcoinUtils.findWitnessCommitment(btcTx, activations); // Assert assertTrue(witnessCommitmentFound.isPresent()); assertEquals(witnessCommitment, witnessCommitmentFound.get()); } - @Test - void findWitnessCommitment_withMultipleWitnessCommitments_shouldReturnLastOne() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withMultipleWitnessCommitments_shouldReturnLastOne(ActivationConfig.ForBlock activations) { // Arrange Sha256Hash witnessCommitment1 = BitcoinTestUtils.createHash(100); Sha256Hash witnessCommitment2 = BitcoinTestUtils.createHash(200); @@ -599,31 +629,33 @@ void findWitnessCommitment_withMultipleWitnessCommitments_shouldReturnLastOne() ); // Act - Optional witnessCommitmentFound = BitcoinUtils.findWitnessCommitment(btcTx); + Optional witnessCommitmentFound = BitcoinUtils.findWitnessCommitment(btcTx, activations); // Assert assertTrue(witnessCommitmentFound.isPresent()); assertEquals(witnessCommitment3, witnessCommitmentFound.get()); } - @Test - void findWitnessCommitment_withWrongWitnessCommitment_shouldReturnEmpty() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withWrongWitnessCommitment_shouldReturnEmpty(ActivationConfig.ForBlock activations) { // Arrange - Sha256Hash fakeWitnessCommitment = BitcoinTestUtils.createHash(101); + Sha256Hash wrongWitnessCommitment = BitcoinTestUtils.createHash(101); BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransactionWithWrongWitnessCommitment( btcMainnetParams, - fakeWitnessCommitment + wrongWitnessCommitment ); // Act - Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx); + Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); // Assert assertFalse(witnessCommitment.isPresent()); } - @Test - void findWitnessCommitment_withRealTransaction_shouldReturnExpectedValue() { + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withRealTransaction_shouldReturnExpectedValue(ActivationConfig.ForBlock activations) { // Arrange // https://mempool.space/tx/b6362fb1369a64c4ce4e0449dc9bd7ffc1ba4fa857fce8a502ff53e49a17c1a7 String rawCoinbaseTx = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5803143c0d1b4d696e656420627920416e74506f6f6c39363552000603188e2f24fabe6d6d0a2f234a30b96bbd9118e926736f18b85f55588455058335d2aa379604b51662100000000000000000004ca30900000000d18300ffffffff05220200000000000017a91442402a28dd61f2718a4b27ae72a4791d5bbdade787e3fd38130000000017a9145249bdf2c131d43995cff42e8feee293f79297a8870000000000000000266a24aa21a9ed434c56197e034947917a46b6c302671f952e0c0ce2ea794fad577b3bbdc0408400000000000000002f6a2d434f524501a37cf4faa0758b26dca666f3e36d42fa15cc01064e3ecda72cb7961caa4b541b1e322bcfe0b5a03000000000000000002b6a2952534b424c4f434b3aebe97391ca0a8447635a588b29295e1fa65aa4723216c21112051f1100683fa50120000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -631,37 +663,137 @@ void findWitnessCommitment_withRealTransaction_shouldReturnExpectedValue() { Sha256Hash expectedWitnessCommitment = Sha256Hash.wrap("434c56197e034947917a46b6c302671f952e0c0ce2ea794fad577b3bbdc04084"); // Act - Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx); + Optional witnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); // Assert assertTrue(witnessCommitment.isPresent()); assertEquals(expectedWitnessCommitment, witnessCommitment.get()); } - } - @Test - void findWitnessCommitment_withDataLargenThanExpected_shouldReturnEmpty() { - // Arrange - BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransaction(btcMainnetParams); - - TransactionWitness txWitness = new TransactionWitness(1); - txWitness.setPush(0, BitcoinTestUtils.WITNESS_RESERVED_VALUE.getBytes()); - btcTx.setWitness(0, txWitness); - - Sha256Hash witnessCommitment = BitcoinTestUtils.createHash(100); - byte[] extraData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; - byte[] witnessCommitmentWithHeaderAndExtraData = ByteUtil.merge( - BitcoinUtils.WITNESS_COMMITMENT_HEADER, - witnessCommitment.getBytes(), - extraData - ); - btcTx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(witnessCommitmentWithHeaderAndExtraData)); - btcTx.verify(); + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withDataLargerThanExpected_shouldReturnEmpty(ActivationConfig.ForBlock activations) { + // Arrange + BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransaction(btcMainnetParams); - // Act - Optional obtainedWitnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx); + TransactionWitness txWitness = new TransactionWitness(1); + txWitness.setPush(0, BitcoinTestUtils.WITNESS_RESERVED_VALUE.getBytes()); + btcTx.setWitness(0, txWitness); + + Sha256Hash witnessCommitment = BitcoinTestUtils.createHash(100); + byte[] extraData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + byte[] witnessCommitmentWithHeaderAndExtraData = ByteUtil.merge( + BitcoinUtils.WITNESS_COMMITMENT_HEADER, + witnessCommitment.getBytes(), + extraData + ); + btcTx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(witnessCommitmentWithHeaderAndExtraData)); + btcTx.verify(); + + // Act + Optional obtainedWitnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); + + // Assert, should not find the commitment since the data length != 36 bytes + assertFalse(obtainedWitnessCommitment.isPresent()); + } + + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withWitnessCommitmentIsShorterThanExpected_shouldReturnEmpty(ActivationConfig.ForBlock activations) { + // Arrange + BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransaction(btcMainnetParams); + + TransactionWitness txWitness = new TransactionWitness(1); + txWitness.setPush(0, BitcoinTestUtils.WITNESS_RESERVED_VALUE.getBytes()); + btcTx.setWitness(0, txWitness); + + byte[] witnessCommitment = BitcoinTestUtils.createHash(100).getBytes(); + + // Create a new witness commitment array with one byte less than the expected 32 bytes + byte[] witnessCommitmentTooShort = new byte[Sha256Hash.LENGTH-1]; + + // Copy the elements from the original array to the new array + System.arraycopy(witnessCommitment, 0, witnessCommitmentTooShort, 0, witnessCommitmentTooShort.length); + + byte[] witnessCommitmentWithHeaderAndExtraData = ByteUtil.merge( + BitcoinUtils.WITNESS_COMMITMENT_HEADER, + witnessCommitmentTooShort + ); + btcTx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(witnessCommitmentWithHeaderAndExtraData)); + btcTx.verify(); + + // Act + Optional obtainedWitnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); + + // Assert, should not find the commitment since the data length != 36 bytes + assertFalse(obtainedWitnessCommitment.isPresent()); + } + + @ParameterizedTest + @MethodSource("activationsProvider") + void findWitnessCommitment_withNoOpReturn_shouldReturnEmpty(ActivationConfig.ForBlock activations) { + // Arrange + BtcTransaction btcTx = BitcoinTestUtils.createCoinbaseTransaction(btcMainnetParams); - // Assert, should not find the commitment since the data length != 36 bytes - assertFalse(obtainedWitnessCommitment.isPresent()); + TransactionWitness txWitness = new TransactionWitness(1); + txWitness.setPush(0, BitcoinTestUtils.WITNESS_RESERVED_VALUE.getBytes()); + btcTx.setWitness(0, txWitness); + + Sha256Hash witnessCommitment = BitcoinTestUtils.createHash(100); + byte[] witnessCommitmentWithHeader = ByteUtil.merge( + BitcoinUtils.WITNESS_COMMITMENT_HEADER, + witnessCommitment.getBytes() + ); + + Script witnessCommitmentOutputScript = (new ScriptBuilder()).op(OP_NOT).data(witnessCommitmentWithHeader) + .build(); + btcTx.addOutput(Coin.ZERO, witnessCommitmentOutputScript); + btcTx.verify(); + + // Act + Optional obtainedWitnessCommitment = BitcoinUtils.findWitnessCommitment(btcTx, activations); + + // Assert, should not find the commitment since the output is not an OP_RETURN + assertFalse(obtainedWitnessCommitment.isPresent()); + } + + @ParameterizedTest + @MethodSource("malFormedCoinbaseTxProvider") + void findWitnessCommitment_whenMalFormedTxBeforeRSKIP460_shouldThrowException(BtcTransaction malFormedCoinbaseTx) { + // Act & Assert + assertThrows(ScriptException.class, () -> BitcoinUtils.findWitnessCommitment(malFormedCoinbaseTx, arrowHeadActivations)); + } + + private static Stream malFormedCoinbaseTxProvider() { + return Stream.of( + Arguments.of(malFormedTestnetCoinbaseTx), + Arguments.of(malFormedMainnetCoinbaseTx), + Arguments.of(coinbaseTxWithWitnessCommitmentOutputInNonStandardFormat) + ); + } + + @ParameterizedTest + @MethodSource("malFormedCoinbaseTxAndExpectedWitnessProvider") + void findWitnessCommitment_whenMalFormedTxAfterRSKIP460_shouldIgnoreMalformedOutputAndReturnFoundWitnessCommitment( + BtcTransaction malFormedCoinbaseTx, Sha256Hash expectedWitnessCommitment) { + // Act + Optional witnessCommitment = BitcoinUtils.findWitnessCommitment( + malFormedCoinbaseTx, allActivations); + + // Assert + assertTrue(witnessCommitment.isPresent()); + assertEquals(expectedWitnessCommitment, witnessCommitment.get()); + } + + private static Stream malFormedCoinbaseTxAndExpectedWitnessProvider() { + return Stream.of( + Arguments.of(malFormedTestnetCoinbaseTx, Sha256Hash.wrap( + "e46b6d3bc71412e8905cedfad91532bdccb693d93a1335fee0b6223a7ed1ee5b")), + Arguments.of(malFormedMainnetCoinbaseTx, Sha256Hash.wrap( + "5e4aae37309d88e9f49d3a4c6fb424e491cbdbac754b8ef06bb90d2a149bd969")), + Arguments.of(coinbaseTxWithWitnessCommitmentOutputInNonStandardFormat, Sha256Hash.wrap( + "4f434b3a8bc552daa25a7a473ac4640ba2b9d621c95652c61488143ca02bbf1b")) + ); + } } } diff --git a/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigTest.java b/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigTest.java index 12f2d7c61a..26d99497b3 100644 --- a/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigTest.java +++ b/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigTest.java @@ -132,6 +132,7 @@ class ActivationConfigTest { " rskip453: lovell700", " rskip454: lovell700", " rskip459: lovell700", + " rskip460: lovell700", "}" )); diff --git a/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigsForTest.java b/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigsForTest.java index 399bb0b54b..a9113de054 100644 --- a/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigsForTest.java +++ b/rskj-core/src/test/java/org/ethereum/config/blockchain/upgrades/ActivationConfigsForTest.java @@ -181,7 +181,8 @@ private static List getLovell700Rskips() { ConsensusRule.RSKIP428, ConsensusRule.RSKIP438, ConsensusRule.RSKIP454, - ConsensusRule.RSKIP459 + ConsensusRule.RSKIP459, + ConsensusRule.RSKIP460 )); }