Skip to content

Commit

Permalink
Merge pull request #2876 from rsksmart/coinbase-refactor
Browse files Browse the repository at this point in the history
Coinbase refactor
  • Loading branch information
aeidelman authored Dec 5, 2024
2 parents 218ca4b + 6c82660 commit 1a073de
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 110 deletions.
131 changes: 107 additions & 24 deletions rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import static co.rsk.peg.BridgeUtils.getRegularPegoutTxSize;
import static co.rsk.peg.ReleaseTransactionBuilder.BTC_TX_VERSION_2;
import static co.rsk.peg.bitcoin.BitcoinUtils.findWitnessCommitment;
import static co.rsk.peg.bitcoin.UtxoUtils.extractOutpointValues;
import static co.rsk.peg.pegin.RejectedPeginReason.INVALID_AMOUNT;
import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*;
Expand Down Expand Up @@ -52,6 +53,7 @@
import co.rsk.peg.whitelist.*;
import co.rsk.rpc.modules.trace.CallType;
import co.rsk.rpc.modules.trace.ProgramSubtrace;
import co.rsk.util.HexUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -60,6 +62,7 @@
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

import org.apache.commons.lang3.tuple.Pair;
import org.ethereum.config.blockchain.upgrades.ActivationConfig;
import org.ethereum.config.blockchain.upgrades.ConsensusRule;
Expand Down Expand Up @@ -1765,7 +1768,7 @@ public Long getBtcTransactionConfirmationsGetCost(Object[] args) {
Context.propagate(btcContext);
try {
this.ensureBtcBlockStore();
final StoredBlock block = btcBlockStore.getFromCache(btcBlockHash);
final StoredBlock block = getBlockKeepingTestnetConsensus(btcBlockHash);

// Block not found, default to basic cost
if (block == null) {
Expand Down Expand Up @@ -1803,7 +1806,7 @@ public Integer getBtcTransactionConfirmations(Sha256Hash btcTxHash, Sha256Hash b
this.ensureBtcBlockChain();

// Get the block using the given block hash
StoredBlock block = btcBlockStore.getFromCache(btcBlockHash);
StoredBlock block = getBlockKeepingTestnetConsensus(btcBlockHash);
if (block == null) {
return BTC_TRANSACTION_CONFIRMATION_INEXISTENT_BLOCK_HASH_ERROR_CODE;
}
Expand Down Expand Up @@ -1839,6 +1842,31 @@ public Integer getBtcTransactionConfirmations(Sha256Hash btcTxHash, Sha256Hash b
return bestChainHeight - block.getHeight() + 1;
}

private StoredBlock getBlockKeepingTestnetConsensus(Sha256Hash btcBlockHash) throws BlockStoreException {
long rskBlockNumber = 5_148_285;

boolean networkIsTestnet = bridgeConstants.getBtcParams().equals(NetworkParameters.fromID(NetworkParameters.ID_TESTNET));
Sha256Hash blockHash = Sha256Hash.wrap("00000000e8e7b540df01a7067e020fd7e2026bf86289def2283a35120c1af379");

// DO NOT MODIFY.
// This check is needed since this block caused a misbehaviour
// for being stored in the cache but not in the storage
if (rskExecutionBlock.getNumber() == rskBlockNumber
&& networkIsTestnet
&& btcBlockHash.equals(blockHash)
) {
byte[] rawBtcBlockHeader = HexUtils.stringHexToByteArray("000000203b5d178405c4e6e7dc07d63d6de5db1342044791721654760c00000000000000796cf6743a036300b43fb3abe6703d04a7999751b6d5744f20327d1175320bd37b954e66ffff001d56dc11ce");

BtcBlock btcBlockHeader = new BtcBlock(bridgeConstants.getBtcParams(), rawBtcBlockHeader);
BigInteger btcBlockChainWork = new BigInteger("000000000000000000000000000000000000000000000ddeb5fbcd969312a77c", 16);
int btcBlockNumber = 2_817_125;

return new StoredBlock(btcBlockHeader, btcBlockChainWork, btcBlockNumber);
}

return btcBlockStore.get(btcBlockHash);
}

private StoredBlock getPrevBlockAtHeight(StoredBlock cursor, int height) throws BlockStoreException {
if (cursor.getHeight() == height) {
return cursor;
Expand Down Expand Up @@ -2144,49 +2172,88 @@ public boolean increaseLockingCap(Transaction tx, Coin newLockingCap) throws Loc
return lockingCapSupport.increaseLockingCap(tx, newLockingCap);
}

public void registerBtcCoinbaseTransaction(byte[] btcTxSerialized, Sha256Hash blockHash, byte[] pmtSerialized, Sha256Hash witnessMerkleRoot, byte[] witnessReservedValue) throws VMException {
public void registerBtcCoinbaseTransaction(
byte[] btcTxSerialized,
Sha256Hash blockHash,
byte[] pmtSerialized,
Sha256Hash witnessMerkleRoot,
byte[] witnessReservedValue
) throws VMException {
Context.propagate(btcContext);
try{
this.ensureBtcBlockStore();
}catch (BlockStoreException | IOException e) {
logger.warn("Exception in registerBtcCoinbaseTransaction", e);
throw new VMException("Exception in registerBtcCoinbaseTransaction", e);
String message = String.format("Exception in registerBtcCoinbaseTransaction. %s", e.getMessage());
logger.warn("[registerBtcCoinbaseTransaction] {}", message);
throw new VMException(message, e);
}

Sha256Hash btcTxHash = BtcTransactionFormatUtils.calculateBtcTxHash(btcTxSerialized);
logger.debug("[registerBtcCoinbaseTransaction] Going to register coinbase information for btcTx: {}", btcTxHash);

if (witnessReservedValue.length != 32) {
logger.warn("[btcTx:{}] WitnessResevedValue length can't be different than 32 bytes", btcTxHash);
throw new BridgeIllegalArgumentException("WitnessResevedValue length can't be different than 32 bytes");
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);
throw new BridgeIllegalArgumentException(message);
}
logger.trace("[registerBtcCoinbaseTransaction] Witness reserved value: {}", Bytes.of(witnessReservedValue));

if (!PartialMerkleTreeFormatUtils.hasExpectedSize(pmtSerialized)) {
logger.warn("[btcTx:{}] PartialMerkleTree doesn't have expected size", btcTxHash);
throw new BridgeIllegalArgumentException("PartialMerkleTree doesn't have expected size");
String message = String.format(
"PartialMerkleTree doesn't have expected size. Value received: %s",
Bytes.of(pmtSerialized)
);
logger.warn("[registerBtcCoinbaseTransaction] {}", message);
throw new BridgeIllegalArgumentException(message);
}

Sha256Hash merkleRoot;

try {
PartialMerkleTree pmt = new PartialMerkleTree(networkParameters, pmtSerialized, 0);
List<Sha256Hash> hashesInPmt = new ArrayList<>();
merkleRoot = pmt.getTxnHashAndMerkleRoot(hashesInPmt);
if (!hashesInPmt.contains(btcTxHash)) {
logger.warn("Supplied Btc Tx {} is not in the supplied partial merkle tree", btcTxHash);
logger.warn(
"[registerBtcCoinbaseTransaction] Supplied btc tx {} is not in the supplied partial merkle tree {}",
btcTxHash,
pmt
);
return;
}
} catch (VerificationException e) {
logger.warn("[btcTx:{}] PartialMerkleTree could not be parsed", btcTxHash);
throw new BridgeIllegalArgumentException(String.format("PartialMerkleTree could not be parsed %s", Bytes.of(pmtSerialized)), e);
String message = String.format("Partial merkle tree could not be parsed. %s", Bytes.of(pmtSerialized));
logger.warn("[registerBtcCoinbaseTransaction] {}", message);
throw new BridgeIllegalArgumentException(message, e);
}
logger.trace("[registerBtcCoinbaseTransaction] Merkle root: {}", 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
StoredBlock storedBlock = btcBlockStore.getFromCache(blockHash);
StoredBlock storedBlock = null;
try {
storedBlock = btcBlockStore.get(blockHash);
} catch (BlockStoreException e) {
logger.error(
"[registerBtcCoinbaseTransaction] Error gettin block {} from block store. {}",
blockHash,
e.getMessage()
);
}

if (storedBlock == null) {
logger.warn("[btcTx:{}] Block not registered", btcTxHash);
throw new BridgeIllegalArgumentException(String.format("Block not registered %s", blockHash.toString()));
String message = String.format("Block %s not yet registered", blockHash);
logger.warn("[registerBtcCoinbaseTransaction] {}", message);
throw new BridgeIllegalArgumentException(message);
}
logger.trace(
"[registerBtcCoinbaseTransaction] Found block with hash {} at height {}",
blockHash,
storedBlock.getHeight()
);

BtcBlock blockHeader = storedBlock.getHeader();
if (!blockHeader.getMerkleRoot().equals(merkleRoot)) {
String panicMessage = String.format(
Expand All @@ -2195,25 +2262,41 @@ public void registerBtcCoinbaseTransaction(byte[] btcTxSerialized, Sha256Hash bl
merkleRoot,
blockHeader.getMerkleRoot()
);
logger.warn(panicMessage);
logger.warn("[registerBtcCoinbaseTransaction] {}", panicMessage);
panicProcessor.panic("btclock", panicMessage);
return;
}

BtcTransaction btcTx = new BtcTransaction(networkParameters, btcTxSerialized);
btcTx.verify();

Sha256Hash witnessCommitment = Sha256Hash.twiceOf(witnessMerkleRoot.getReversedBytes(), witnessReservedValue);

if(!witnessCommitment.equals(btcTx.findWitnessCommitment())){
logger.warn("[btcTx:{}] WitnessCommitment does not match", btcTxHash);
throw new BridgeIllegalArgumentException("WitnessCommitment does not match");
}
validateWitnessInformation(btcTx, witnessMerkleRoot, witnessReservedValue);

CoinbaseInformation coinbaseInformation = new CoinbaseInformation(witnessMerkleRoot);
provider.setCoinbaseInformation(blockHeader.getHash(), coinbaseInformation);

logger.warn("[btcTx:{}] Registered coinbase information", btcTxHash);
logger.debug("[registerBtcCoinbaseTransaction] Registered coinbase information for btc tx {}", btcTxHash);
}

private void validateWitnessInformation(
BtcTransaction coinbaseTransaction,
Sha256Hash witnessMerkleRoot,
byte[] witnessReservedValue
) throws BridgeIllegalArgumentException {
Optional<Sha256Hash> expectedWitnessCommitment = findWitnessCommitment(coinbaseTransaction);
Sha256Hash calculatedWitnessCommitment = Sha256Hash.twiceOf(witnessMerkleRoot.getReversedBytes(), witnessReservedValue);

if (expectedWitnessCommitment.isEmpty() || !expectedWitnessCommitment.get().equals(calculatedWitnessCommitment)) {
String message = String.format(
"[btcTx: %s] Witness commitment does not match. Expected: %s, Calculated: %s",
coinbaseTransaction.getHash(),
expectedWitnessCommitment.orElse(null),
calculatedWitnessCommitment
);
logger.warn("[validateWitnessInformation] {}", message);
throw new BridgeIllegalArgumentException(message);
}
logger.debug("[validateWitnessInformation] Witness commitment {} validated for btc tx {}", calculatedWitnessCommitment, coinbaseTransaction.getHash());
}

public boolean hasBtcBlockCoinbaseTransactionInformation(Sha256Hash blockHash) {
Expand Down
9 changes: 2 additions & 7 deletions rskj-core/src/main/java/co/rsk/peg/BridgeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@

import co.rsk.bitcoinj.core.*;
import co.rsk.bitcoinj.crypto.TransactionSignature;
import co.rsk.bitcoinj.script.RedeemScriptParserFactory;
import co.rsk.bitcoinj.script.Script;
import co.rsk.bitcoinj.script.ScriptChunk;
import co.rsk.bitcoinj.script.*;
import co.rsk.bitcoinj.wallet.Wallet;
import co.rsk.peg.constants.BridgeConstants;
import co.rsk.core.RskAddress;
Expand All @@ -48,10 +46,7 @@

import javax.annotation.Nonnull;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP284;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ public synchronized StoredBlock get(Sha256Hash hash) {
logger.trace("[get] Block with hash {} not found in storage", hash);
return null;
}
StoredBlock storedBlock = byteArrayToStoredBlock(ba);
return storedBlock;
return byteArrayToStoredBlock(ba);
}

@Override
Expand Down Expand Up @@ -200,7 +199,7 @@ public StoredBlock getStoredBlockAtMainChainHeight(int height) throws BlockStore

if (depth < 0) {
String message = String.format(
"Height provided is higher than chain head. provided: %n. chain head: %n",
"Height provided is higher than chain head. provided: %s. chain head: %s",
height,
chainHead.getHeight()
);
Expand Down
56 changes: 45 additions & 11 deletions rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package co.rsk.peg.bitcoin;

import co.rsk.bitcoinj.core.BtcTransaction;
import co.rsk.bitcoinj.core.ScriptException;
import co.rsk.bitcoinj.core.Sha256Hash;
import co.rsk.bitcoinj.core.TransactionInput;
import co.rsk.bitcoinj.script.Script;
import co.rsk.bitcoinj.script.ScriptBuilder;
import co.rsk.bitcoinj.script.ScriptChunk;
import static co.rsk.bitcoinj.script.ScriptOpCodes.OP_RETURN;

import co.rsk.bitcoinj.core.*;
import co.rsk.bitcoinj.script.*;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.util.*;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Optional;

public class BitcoinUtils {

public static final byte WITNESS_COMMITMENT_LENGTH = 36; // 4 bytes for header, 32 for hash
public static final Sha256Hash WITNESS_RESERVED_VALUE = Sha256Hash.ZERO_HASH;
protected static final byte[] WITNESS_COMMITMENT_HEADER = Hex.decode("aa21a9ed");
private static final Logger logger = LoggerFactory.getLogger(BitcoinUtils.class);
private static final int FIRST_INPUT_INDEX = 0;

Expand Down Expand Up @@ -81,4 +81,38 @@ public static void removeSignaturesFromTransactionWithP2shMultiSigInputs(BtcTran
input.setScriptSig(emptyInputScript);
}
}

public static Optional<Sha256Hash> findWitnessCommitment(BtcTransaction tx) {
Preconditions.checkState(tx.isCoinBase());
// If more than one witness commitment, take the last one as the valid one
List<TransactionOutput> outputsReversed = Lists.reverse(tx.getOutputs());

for (TransactionOutput output : outputsReversed) {
Script scriptPubKey = output.getScriptPubKey();
if (isWitnessCommitment(scriptPubKey)) {
Sha256Hash witnessCommitment = ScriptPattern.extractWitnessCommitmentHash(scriptPubKey);
return Optional.of(witnessCommitment);
}
}

return Optional.empty();
}

private static boolean isWitnessCommitment(Script scriptPubKey) {
final int MINIMUM_WITNESS_COMMITMENT_SIZE = 38;
byte[] scriptPubKeyProgram = scriptPubKey.getProgram();

return scriptPubKeyProgram.length >= MINIMUM_WITNESS_COMMITMENT_SIZE
&& hasCommitmentStructure(scriptPubKeyProgram);
}

private static boolean hasCommitmentStructure(byte[] scriptPubKeyProgram) {
return scriptPubKeyProgram[0] == OP_RETURN
&& scriptPubKeyProgram[1] == WITNESS_COMMITMENT_LENGTH
&& hasWitnessCommitmentHeader(Arrays.copyOfRange(scriptPubKeyProgram, 2, 6));
}

private static boolean hasWitnessCommitmentHeader(byte[] header) {
return Arrays.equals(header, WITNESS_COMMITMENT_HEADER);
}
}
14 changes: 7 additions & 7 deletions rskj-core/src/test/java/co/rsk/peg/BridgeSupportIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -3543,7 +3543,7 @@ void getBtcTransactionConfirmations_blockNotInBestChain() throws BlockStoreExcep
StoredBlock block = new StoredBlock(blockHeader, new BigInteger("0"), height);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

StoredBlock chainHead = new StoredBlock(blockHeader, new BigInteger("0"), 132);
when(btcBlockStore.getChainHead()).thenReturn(chainHead);
Expand Down Expand Up @@ -3587,11 +3587,11 @@ void getBtcTransactionConfirmations_blockNotInBestChainBlockWithHeightNotFound()
StoredBlock block = new StoredBlock(blockHeader, new BigInteger("0"), 50);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

StoredBlock chainHead = new StoredBlock(blockHeader, new BigInteger("0"), 132);
when(btcBlockStore.getChainHead()).thenReturn(chainHead);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

BridgeStorageProvider provider = new BridgeStorageProvider(
track,
Expand Down Expand Up @@ -3632,7 +3632,7 @@ void getBtcTransactionConfirmations_blockTooOld() throws BlockStoreException, IO
StoredBlock block = new StoredBlock(blockHeader, new BigInteger("0"), BLOCK_HEIGHT);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

StoredBlock chainHead = new StoredBlock(blockHeader, new BigInteger("0"), BLOCK_HEIGHT + 4320 + 1);
when(btcBlockStore.getChainHead()).thenReturn(chainHead);
Expand Down Expand Up @@ -3675,7 +3675,7 @@ void getBtcTransactionConfirmations_heightInconsistency() throws BlockStoreExcep
StoredBlock block = new StoredBlock(blockHeader, new BigInteger("0"), height);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

StoredBlock chainHead = new StoredBlock(blockHeader, new BigInteger("0"), 132);
when(btcBlockStore.getChainHead()).thenReturn(chainHead);
Expand Down Expand Up @@ -3721,7 +3721,7 @@ void getBtcTransactionConfirmations_merkleBranchDoesNotProve() throws BlockStore
StoredBlock block = new StoredBlock(blockHeader, new BigInteger("0"), height);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

StoredBlock chainHead = new StoredBlock(blockHeader, new BigInteger("0"), 132);
when(btcBlockStore.getChainHead()).thenReturn(chainHead);
Expand Down Expand Up @@ -3762,7 +3762,7 @@ void getBtcTransactionConfirmationsGetCost_ok() throws BlockStoreException {
StoredBlock block = new StoredBlock(null, new BigInteger("0"), 50);

BtcBlockStoreWithCache btcBlockStore = mock(BtcBlockStoreWithCache.class);
when(btcBlockStore.getFromCache(blockHash)).thenReturn(block);
when(btcBlockStore.get(blockHash)).thenReturn(block);

BtcBlock btcBlock = mock(BtcBlock.class);
doReturn(Sha256Hash.of(Hex.decode("aa"))).when(btcBlock).getHash();
Expand Down
Loading

0 comments on commit 1a073de

Please sign in to comment.