Skip to content

Commit

Permalink
Merge pull request #2381 from rsksmart/hsm-segwit-compatibility-integ…
Browse files Browse the repository at this point in the history
…ration

New pegout creation event including UTXO outpoint values
  • Loading branch information
josedahlquist authored Jun 6, 2024
2 parents 07e9c13 + 2759629 commit abf22ed
Show file tree
Hide file tree
Showing 20 changed files with 1,201 additions and 133 deletions.
7 changes: 7 additions & 0 deletions rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ public enum BridgeEvents {
new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)),
new CallTransaction.Param(false, "pegoutCreationRskBlockNumber", SolidityType.getType(SolidityType.UINT256))
}
),
PEGOUT_TRANSACTION_CREATED("pegout_transaction_created",
new CallTransaction.Param[]{
new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)),
new CallTransaction.Param(false, Fields.UTXO_OUTPOINT_VALUES, SolidityType.getType(SolidityType.BYTES))
}
);

private String eventName;
Expand All @@ -121,5 +127,6 @@ private static class Fields {
private static final String BTC_TX_HASH = "btcTxHash";
private static final String RELEASE_RSK_TX_HASH = "releaseRskTxHash";
private static final String RELEASE_RSK_TX_HASHES = "releaseRskTxHashes";
private static final String UTXO_OUTPOINT_VALUES = "utxoOutpointValues";
}
}
158 changes: 92 additions & 66 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.UtxoUtils.extractOutpointValues;
import static co.rsk.peg.pegin.RejectedPeginReason.INVALID_AMOUNT;
import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*;

Expand Down Expand Up @@ -1041,7 +1042,7 @@ private void migrateFunds(

PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations();
Pair<BtcTransaction, List<UTXO>> createResult = createMigrationTransaction(retiringFederationWallet, activeFederationAddress);
BtcTransaction btcTx = createResult.getLeft();
BtcTransaction migrationTransaction = createResult.getLeft();
List<UTXO> selectedUTXOs = createResult.getRight();

logger.debug(
Expand All @@ -1054,26 +1055,33 @@ private void migrateFunds(
Coin amountMigrated = selectedUTXOs.stream()
.map(UTXO::getValue)
.reduce(Coin.ZERO, Coin::add);
pegoutsWaitingForConfirmations.add(btcTx, rskExecutionBlock.getNumber(), rskTxHash);
pegoutsWaitingForConfirmations.add(migrationTransaction, rskExecutionBlock.getNumber(), rskTxHash);
// Log the Release request
logger.debug(
"[migrateFunds] release requested. rskTXHash: {}, btcTxHash: {}, amount: {}",
rskTxHash,
btcTx.getHash(),
migrationTransaction.getHash(),
amountMigrated
);
eventLogger.logReleaseBtcRequested(rskTxHash.getBytes(), btcTx, amountMigrated);
eventLogger.logReleaseBtcRequested(rskTxHash.getBytes(), migrationTransaction, amountMigrated);
} else {
pegoutsWaitingForConfirmations.add(btcTx, rskExecutionBlock.getNumber());
pegoutsWaitingForConfirmations.add(migrationTransaction, rskExecutionBlock.getNumber());
}

// Store pegoutTxSigHash to be able to identify the tx type
savePegoutTxSigHash(btcTx);
savePegoutTxSigHash(migrationTransaction);

// Mark UTXOs as spent
availableUTXOs.removeIf(utxo -> selectedUTXOs.stream().anyMatch(selectedUtxo ->
utxo.getHash().equals(selectedUtxo.getHash()) && utxo.getIndex() == selectedUtxo.getIndex()
));

if (!activations.isActive(RSKIP428)) {
return;
}

List<Coin> outpointValues = extractOutpointValues(migrationTransaction);
eventLogger.logPegoutTransactionCreated(migrationTransaction.getHash(), outpointValues);
}

/**
Expand Down Expand Up @@ -1188,64 +1196,74 @@ private void processPegoutsInBatch(
long currentBlockNumber = rskExecutionBlock.getNumber();
long nextPegoutCreationBlockNumber = getNextPegoutCreationBlockNumber();

if (currentBlockNumber >= nextPegoutCreationBlockNumber) {
List<ReleaseRequestQueue.Entry> pegoutEntries = pegoutRequests.getEntries();
Coin totalPegoutValue = pegoutEntries
.stream()
.map(ReleaseRequestQueue.Entry::getAmount)
.reduce(Coin.ZERO, Coin::add);
if (currentBlockNumber < nextPegoutCreationBlockNumber) {
return;
}

List<ReleaseRequestQueue.Entry> pegoutEntries = pegoutRequests.getEntries();
Coin totalPegoutValue = pegoutEntries
.stream()
.map(ReleaseRequestQueue.Entry::getAmount)
.reduce(Coin.ZERO, Coin::add);

if (wallet.getBalance().isLessThan(totalPegoutValue)) {
logger.warn("[processPegoutsInBatch] wallet balance {} is less than the totalPegoutValue {}", wallet.getBalance(), totalPegoutValue);
return;
}
if (wallet.getBalance().isLessThan(totalPegoutValue)) {
logger.warn("[processPegoutsInBatch] wallet balance {} is less than the totalPegoutValue {}", wallet.getBalance(), totalPegoutValue);
return;
}

if (!pegoutEntries.isEmpty()) {
logger.info("[processPegoutsInBatch] going to create a batched pegout transaction for {} requests, total amount {}", pegoutEntries.size(), totalPegoutValue);
ReleaseTransactionBuilder.BuildResult result = txBuilder.buildBatchedPegouts(pegoutEntries);
if (!pegoutEntries.isEmpty()) {
logger.info("[processPegoutsInBatch] going to create a batched pegout transaction for {} requests, total amount {}", pegoutEntries.size(), totalPegoutValue);
ReleaseTransactionBuilder.BuildResult result = txBuilder.buildBatchedPegouts(pegoutEntries);

while (pegoutEntries.size() > 1 && result.getResponseCode() == ReleaseTransactionBuilder.Response.EXCEED_MAX_TRANSACTION_SIZE) {
logger.info("[processPegoutsInBatch] Max size exceeded, going to divide {} requests in half", pegoutEntries.size());
int firstHalfSize = pegoutEntries.size() / 2;
pegoutEntries = pegoutEntries.subList(0, firstHalfSize);
result = txBuilder.buildBatchedPegouts(pegoutEntries);
}
while (pegoutEntries.size() > 1 && result.getResponseCode() == ReleaseTransactionBuilder.Response.EXCEED_MAX_TRANSACTION_SIZE) {
logger.info("[processPegoutsInBatch] Max size exceeded, going to divide {} requests in half", pegoutEntries.size());
int firstHalfSize = pegoutEntries.size() / 2;
pegoutEntries = pegoutEntries.subList(0, firstHalfSize);
result = txBuilder.buildBatchedPegouts(pegoutEntries);
}

if (result.getResponseCode() != ReleaseTransactionBuilder.Response.SUCCESS) {
logger.warn(
"Couldn't build a pegout BTC tx for {} pending requests (total amount: {}), Reason: {}",
pegoutRequests.getEntries().size(),
totalPegoutValue,
result.getResponseCode());
return;
}
if (result.getResponseCode() != ReleaseTransactionBuilder.Response.SUCCESS) {
logger.warn(
"Couldn't build a pegout BTC tx for {} pending requests (total amount: {}), Reason: {}",
pegoutRequests.getEntries().size(),
totalPegoutValue,
result.getResponseCode());
return;
}

logger.info("[processPegoutsInBatch] pegouts processed with btcTx hash {} and response code {}", result.getBtcTx().getHash(), result.getResponseCode());
logger.info(
"[processPegoutsInBatch] pegouts processed with btcTx hash {} and response code {}",
result.getBtcTx().getHash(), result.getResponseCode());

BtcTransaction generatedTransaction = result.getBtcTx();
addToPegoutsWaitingForConfirmations(generatedTransaction, pegoutsWaitingForConfirmations, rskTx.getHash(), totalPegoutValue);
savePegoutTxSigHash(generatedTransaction);
BtcTransaction batchPegoutTransaction = result.getBtcTx();
addToPegoutsWaitingForConfirmations(batchPegoutTransaction,
pegoutsWaitingForConfirmations, rskTx.getHash(), totalPegoutValue);
savePegoutTxSigHash(batchPegoutTransaction);

// Remove batched requests from the queue after successfully batching pegouts
pegoutRequests.removeEntries(pegoutEntries);
// Remove batched requests from the queue after successfully batching pegouts
pegoutRequests.removeEntries(pegoutEntries);

// Mark UTXOs as spent
List<UTXO> selectedUTXOs = result.getSelectedUTXOs();
logger.debug("[processPegoutsInBatch] used {} UTXOs for this pegout", selectedUTXOs.size());
availableUTXOs.removeAll(selectedUTXOs);
// Mark UTXOs as spent
List<UTXO> selectedUTXOs = result.getSelectedUTXOs();
logger.debug("[processPegoutsInBatch] used {} UTXOs for this pegout", selectedUTXOs.size());
availableUTXOs.removeAll(selectedUTXOs);

eventLogger.logBatchPegoutCreated(generatedTransaction.getHash(),
pegoutEntries.stream().map(ReleaseRequestQueue.Entry::getRskTxHash).collect(Collectors.toList()));
eventLogger.logBatchPegoutCreated(batchPegoutTransaction.getHash(),
pegoutEntries.stream().map(ReleaseRequestQueue.Entry::getRskTxHash).collect(Collectors.toList()));

adjustBalancesIfChangeOutputWasDust(generatedTransaction, totalPegoutValue, wallet);
if (activations.isActive(RSKIP428)) {
List<Coin> outpointValues = extractOutpointValues(batchPegoutTransaction);
eventLogger.logPegoutTransactionCreated(batchPegoutTransaction.getHash(), outpointValues);
}

// update next Pegout height even if there were no request in queue
if (pegoutRequests.getEntries().isEmpty()) {
long nextPegoutHeight = currentBlockNumber + bridgeConstants.getNumberOfBlocksBetweenPegouts();
provider.setNextPegoutHeight(nextPegoutHeight);
logger.info("[processPegoutsInBatch] Next Pegout Height updated from {} to {}", currentBlockNumber, nextPegoutHeight);
}
adjustBalancesIfChangeOutputWasDust(batchPegoutTransaction, totalPegoutValue, wallet);
}

// set the next pegout creation block number when there are no pending pegout requests to be processed or they have been already processed
if (pegoutRequests.getEntries().isEmpty()) {
long nextPegoutHeight = currentBlockNumber + bridgeConstants.getNumberOfBlocksBetweenPegouts();
provider.setNextPegoutHeight(nextPegoutHeight);
logger.info("[processPegoutsInBatch] Next Pegout Height updated from {} to {}", currentBlockNumber, nextPegoutHeight);
}
}

Expand Down Expand Up @@ -3220,20 +3238,7 @@ private void generateRejectionRelease(
);

ReleaseTransactionBuilder.BuildResult buildReturnResult = txBuilder.buildEmptyWalletTo(btcRefundAddress);
if (buildReturnResult.getResponseCode() == ReleaseTransactionBuilder.Response.SUCCESS) {
if (activations.isActive(ConsensusRule.RSKIP146)) {
provider.getPegoutsWaitingForConfirmations().add(buildReturnResult.getBtcTx(), rskExecutionBlock.getNumber(), rskTxHash);
eventLogger.logReleaseBtcRequested(rskTxHash.getBytes(), buildReturnResult.getBtcTx(), totalAmount);
} else {
provider.getPegoutsWaitingForConfirmations().add(buildReturnResult.getBtcTx(), rskExecutionBlock.getNumber());
}
logger.info(
"[generateRejectionRelease] Rejecting peg-in tx built successfully: Refund to address: {}. RskTxHash: {}. Value {}.",
btcRefundAddress,
rskTxHash,
totalAmount
);
} else {
if (buildReturnResult.getResponseCode() != ReleaseTransactionBuilder.Response.SUCCESS) {
logger.warn(
"[generateRejectionRelease] Rejecting peg-in tx could not be built due to {}: Btc peg-in txHash {}. Refund to address: {}. RskTxHash: {}. Value: {}",
buildReturnResult.getResponseCode(),
Expand All @@ -3243,7 +3248,28 @@ private void generateRejectionRelease(
totalAmount
);
panicProcessor.panic("peg-in-refund", String.format("peg-in money return tx build for btc tx %s error. Return was to %s. Tx %s. Value %s. Reason %s", btcTx.getHash(), btcRefundAddress, rskTxHash, totalAmount, buildReturnResult.getResponseCode()));
return;
}

BtcTransaction refundPegoutTransaction = buildReturnResult.getBtcTx();
if (activations.isActive(ConsensusRule.RSKIP146)) {
provider.getPegoutsWaitingForConfirmations().add(refundPegoutTransaction, rskExecutionBlock.getNumber(), rskTxHash);
eventLogger.logReleaseBtcRequested(rskTxHash.getBytes(), refundPegoutTransaction, totalAmount);
} else {
provider.getPegoutsWaitingForConfirmations().add(refundPegoutTransaction, rskExecutionBlock.getNumber());
}

if (activations.isActive(RSKIP428)) {
List<Coin> outpointValues = extractOutpointValues(refundPegoutTransaction);
eventLogger.logPegoutTransactionCreated(refundPegoutTransaction.getHash(), outpointValues);
}

logger.info(
"[generateRejectionRelease] Rejecting peg-in tx built successfully: Refund to address: {}. RskTxHash: {}. Value {}.",
btcRefundAddress,
rskTxHash,
totalAmount
);
}

private void generateRejectionRelease(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.rsk.peg.bitcoin;

public class InvalidOutpointValueException extends RuntimeException {

public InvalidOutpointValueException(String message) {
super(message);
}

public InvalidOutpointValueException(String message, Throwable cause) {
super(message, cause);
}
}
106 changes: 106 additions & 0 deletions rskj-core/src/main/java/co/rsk/peg/bitcoin/UtxoUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package co.rsk.peg.bitcoin;

import co.rsk.bitcoinj.core.BtcTransaction;
import co.rsk.bitcoinj.core.Coin;
import co.rsk.bitcoinj.core.TransactionInput;
import co.rsk.bitcoinj.core.VarInt;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.spongycastle.util.encoders.Hex;

public final class UtxoUtils {

private UtxoUtils() {
}

/**
* Decode a {@code byte[]} of encoded outpoint values.
*
* @param encodedOutpointValues
* @return {@code List<Coin>} the list of outpoint values decoded preserving
* the order of the entries. Or an {@code Collections.EMPTY_LIST} when {@code encodedOutpointValues} is
* {@code null} or {@code empty byte[]}.
*/
public static List<Coin> decodeOutpointValues(byte[] encodedOutpointValues) {
if (encodedOutpointValues == null || encodedOutpointValues.length == 0) {
return Collections.emptyList();
}
int offset = 0;
List<Coin> outpointValues = new ArrayList<>();

while (encodedOutpointValues.length > offset) {
VarInt valueAsVarInt;
try {
valueAsVarInt = new VarInt(encodedOutpointValues, offset);
} catch (Exception ex) {
throw new InvalidOutpointValueException(
String.format("Invalid value with invalid VarInt format: %s",
Hex.toHexString(encodedOutpointValues).toUpperCase()
),
ex
);
}

offset += valueAsVarInt.getSizeInBytes();
Coin outpointValue = Coin.valueOf(valueAsVarInt.value);
validateOutpointValue(outpointValue);

outpointValues.add(outpointValue);
}
return outpointValues;

}

/**
* Encode a {@code List<Coin} of outpoint values.
*
* @param outpointValues
* @return {@code byte[]} the list of outpoint values encoded preserving the order of the
* entries. Or an {@code empty byte[]} when {@code outpointValues} is {@code null} or
* {@code empty}.
*/
public static byte[] encodeOutpointValues(List<Coin> outpointValues) {
if (outpointValues == null || outpointValues.isEmpty()) {
return new byte[]{};
}

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (Coin outpointValue : outpointValues) {
validateOutpointValue(outpointValue);
VarInt varIntOutpointValue = new VarInt(outpointValue.getValue());
try {
outputStream.write(varIntOutpointValue.encode());
} catch (IOException ex) {
throw new InvalidOutpointValueException(
String.format("I/O exception for value: %s",
outpointValue
),
ex
);
}
}
return outputStream.toByteArray();
}

private static void validateOutpointValue(Coin outpointValue) {
if (outpointValue == null || outpointValue.isNegative()) {
throw new InvalidOutpointValueException(String.format(
"Invalid outpoint value: %s. Negative and null values are not allowed.",
outpointValue));
}
}

public static List<Coin> extractOutpointValues(BtcTransaction generatedTransaction) {
if (generatedTransaction == null) {
return Collections.emptyList();
}

return generatedTransaction.getInputs().stream().map(TransactionInput::getValue).collect(
Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,8 @@ default void logBatchPegoutCreated(Sha256Hash btcTxHash, List<Keccak256> rskTxHa
default void logPegoutConfirmed(Sha256Hash btcTxHash, long pegoutCreationRskBlockNumber) {
throw new UnsupportedOperationException();
}

default void logPegoutTransactionCreated(Sha256Hash btcTxHash, List<Coin> outpointValues) {
throw new UnsupportedOperationException();
}
}
Loading

0 comments on commit abf22ed

Please sign in to comment.