Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create method to remove utxos used for a release transaction #2913

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 61 additions & 47 deletions rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,9 @@ private void processSvpFundTransactionUnsigned(Keccak256 rskTxHash, Federation p
BtcTransaction svpFundTransactionUnsigned = createSvpFundTransaction(proposedFederation, spendableValueFromProposedFederation);
provider.setSvpFundTxHashUnsigned(svpFundTransactionUnsigned.getHash());
PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations();
settleReleaseRequest(pegoutsWaitingForConfirmations, svpFundTransactionUnsigned, rskTxHash, spendableValueFromProposedFederation);

List<UTXO> utxosToUse = federationSupport.getActiveFederationBtcUTXOs();
settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, svpFundTransactionUnsigned, rskTxHash, spendableValueFromProposedFederation);
} catch (InsufficientMoneyException e) {
logger.error(
"[processSvpFundTransactionUnsigned] Insufficient funds for creating the fund transaction. Error message: {}",
Expand Down Expand Up @@ -1138,46 +1140,59 @@ private SendRequest createSvpFundTransactionSendRequest(BtcTransaction transacti
}

private void processSvpSpendTransactionUnsigned(Keccak256 rskTxHash, Federation proposedFederation, BtcTransaction svpFundTxSigned) {
BtcTransaction svpSpendTransactionUnsigned = createSvpSpendTransaction(svpFundTxSigned, proposedFederation);
BtcTransaction svpSpendTransactionUnsigned;
try {
svpSpendTransactionUnsigned = createSvpSpendTransaction(svpFundTxSigned, proposedFederation);
} catch (IllegalStateException e){
logger.error("[processSvpSpendTransactionUnsigned] Error creating spend transaction {}", e.getMessage());
return;
}
updateSvpSpendTransactionValues(rskTxHash, svpSpendTransactionUnsigned);

Coin amountSentToActiveFed = svpSpendTransactionUnsigned.getOutput(0).getValue();
logReleaseRequested(rskTxHash, svpSpendTransactionUnsigned, amountSentToActiveFed);
logPegoutTransactionCreated(svpSpendTransactionUnsigned);
}

private BtcTransaction createSvpSpendTransaction(BtcTransaction svpFundTxSigned, Federation proposedFederation) {
private BtcTransaction createSvpSpendTransaction(BtcTransaction svpFundTxSigned, Federation proposedFederation) throws IllegalStateException {
BtcTransaction svpSpendTransaction = new BtcTransaction(networkParameters);
svpSpendTransaction.setVersion(BTC_TX_VERSION_2);

addSvpSpendTransactionInputs(svpSpendTransaction, svpFundTxSigned, proposedFederation);
Script proposedFederationRedeemScript = proposedFederation.getRedeemScript();
TransactionOutput outputToProposedFed = searchForOutput(
svpFundTxSigned.getOutputs(),
proposedFederation.getP2SHScript()
).orElseThrow(() -> new IllegalStateException("[createSvpSpendTransaction] Output to proposed federation was not found in fund transaction."));
svpSpendTransaction.addInput(outputToProposedFed);
svpSpendTransaction.getInput(0).setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(proposedFederationRedeemScript));

Script flyoverRedeemScript = getFlyoverRedeemScript(bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederationRedeemScript);
Script flyoverOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript);
TransactionOutput outputToFlyoverProposedFed = searchForOutput(
svpFundTxSigned.getOutputs(),
flyoverOutputScript
).orElseThrow(() -> new IllegalStateException("[createSvpSpendTransaction] Output to flyover proposed federation was not found in fund transaction."));
svpSpendTransaction.addInput(outputToFlyoverProposedFed);
svpSpendTransaction.getInput(1).setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(flyoverRedeemScript));

Coin valueSentToProposedFed = outputToProposedFed.getValue();
Coin valueSentToFlyoverProposedFed = outputToFlyoverProposedFed.getValue();

Coin valueToSend = valueSentToProposedFed
.plus(valueSentToFlyoverProposedFed)
.minus(calculateSvpSpendTxFees(proposedFederation));

svpSpendTransaction.addOutput(
calculateSvpSpendTxAmount(proposedFederation),
valueToSend,
federationSupport.getActiveFederationAddress()
);

return svpSpendTransaction;
}

private void addSvpSpendTransactionInputs(BtcTransaction svpSpendTransaction, BtcTransaction svpFundTxSigned, Federation proposedFederation) {
Script proposedFederationRedeemScript = proposedFederation.getRedeemScript();
Script proposedFederationOutputScript = proposedFederation.getP2SHScript();
addInputFromMatchingOutputScript(svpSpendTransaction, svpFundTxSigned, proposedFederationOutputScript);
svpSpendTransaction.getInput(0)
.setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(proposedFederationRedeemScript));

Script flyoverRedeemScript =
getFlyoverRedeemScript(bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederationRedeemScript);
Script flyoverOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript);
addInputFromMatchingOutputScript(svpSpendTransaction, svpFundTxSigned, flyoverOutputScript);
svpSpendTransaction.getInput(1)
.setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(flyoverRedeemScript));
}

private Coin calculateSvpSpendTxAmount(Federation proposedFederation) {
private Coin calculateSvpSpendTxFees(Federation proposedFederation) {
int svpSpendTransactionSize = calculatePegoutTxSize(activations, proposedFederation, 2, 1);
long svpSpendTransactionBackedUpSize = svpSpendTransactionSize * 12L / 10L; // just to be sure the amount sent will be enough
long svpSpendTransactionBackedUpSize = svpSpendTransactionSize * 12L / 10L; // just to be sure the fees sent will be enough

return feePerKbSupport.getFeePerKb()
.multiply(svpSpendTransactionBackedUpSize)
Expand Down Expand Up @@ -1291,7 +1306,7 @@ private void migrateFunds(
Keccak256 rskTxHash,
Wallet retiringFederationWallet,
Address activeFederationAddress,
List<UTXO> availableUTXOs) throws IOException {
List<UTXO> utxosToUse) throws IOException {

PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations();
Pair<BtcTransaction, List<UTXO>> createResult = createMigrationTransaction(retiringFederationWallet, activeFederationAddress);
Expand All @@ -1306,12 +1321,8 @@ private void migrateFunds(
Coin amountMigrated = selectedUTXOs.stream()
.map(UTXO::getValue)
.reduce(Coin.ZERO, Coin::add);
settleReleaseRequest(pegoutsWaitingForConfirmations, migrationTransaction, rskTxHash, amountMigrated);

// Mark UTXOs as spent
availableUTXOs.removeIf(utxo -> selectedUTXOs.stream().anyMatch(selectedUtxo ->
utxo.getHash().equals(selectedUtxo.getHash()) && utxo.getIndex() == selectedUtxo.getIndex()
));
settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, migrationTransaction, rskTxHash, amountMigrated);
}

/**
Expand Down Expand Up @@ -1357,11 +1368,23 @@ private void processPegoutRequests(Transaction rskTx) {
}
}

private void settleReleaseRequest(PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction pegoutTransaction, Keccak256 releaseCreationTxHash, Coin requestedAmount) {
addPegoutToPegoutsWaitingForConfirmations(pegoutsWaitingForConfirmations, pegoutTransaction, releaseCreationTxHash);
savePegoutTxSigHash(pegoutTransaction);
logReleaseRequested(releaseCreationTxHash, pegoutTransaction, requestedAmount);
logPegoutTransactionCreated(pegoutTransaction);
private void settleReleaseRequest(List<UTXO> utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction releaseTransaction, Keccak256 releaseCreationTxHash, Coin requestedAmount) {
removeSpentUtxos(utxosToUse, releaseTransaction);
addPegoutToPegoutsWaitingForConfirmations(pegoutsWaitingForConfirmations, releaseTransaction, releaseCreationTxHash);
savePegoutTxSigHash(releaseTransaction);
logReleaseRequested(releaseCreationTxHash, releaseTransaction, requestedAmount);
logPegoutTransactionCreated(releaseTransaction);
}

private void removeSpentUtxos(List<UTXO> utxosToUse, BtcTransaction releaseTx) {
List<UTXO> utxosToRemove = utxosToUse.stream()
.filter(utxo -> releaseTx.getInputs().stream().anyMatch(input ->
input.getOutpoint().getHash().equals(utxo.getHash()) && input.getOutpoint().getIndex() == utxo.getIndex())
).toList();

logger.debug("[removeSpentUtxos] Used {} UTXOs for this release", utxosToRemove.size());

utxosToUse.removeAll(utxosToRemove);
}

private void addPegoutToPegoutsWaitingForConfirmations(PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction pegoutTransaction, Keccak256 releaseCreationTxHash) {
Expand Down Expand Up @@ -1419,7 +1442,7 @@ private void logPegoutTransactionCreated(BtcTransaction pegoutTransaction) {
private void processPegoutsIndividually(
ReleaseRequestQueue pegoutRequests,
ReleaseTransactionBuilder txBuilder,
List<UTXO> availableUTXOs,
List<UTXO> utxosToUse,
PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations,
Wallet wallet
) {
Expand All @@ -1444,11 +1467,7 @@ private void processPegoutsIndividually(

BtcTransaction generatedTransaction = result.getBtcTx();
Keccak256 pegoutCreationTxHash = pegoutRequest.getRskTxHash();
settleReleaseRequest(pegoutsWaitingForConfirmations, generatedTransaction, pegoutCreationTxHash, pegoutRequest.getAmount());

// Mark UTXOs as spent
List<UTXO> selectedUTXOs = result.getSelectedUTXOs();
availableUTXOs.removeAll(selectedUTXOs);
settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, generatedTransaction, pegoutCreationTxHash, pegoutRequest.getAmount());

adjustBalancesIfChangeOutputWasDust(generatedTransaction, pegoutRequest.getAmount(), wallet);

Expand All @@ -1459,7 +1478,7 @@ private void processPegoutsIndividually(
private void processPegoutsInBatch(
ReleaseRequestQueue pegoutRequests,
ReleaseTransactionBuilder txBuilder,
List<UTXO> availableUTXOs,
List<UTXO> utxosToUse,
PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations,
Wallet wallet,
Transaction rskTx) {
Expand Down Expand Up @@ -1508,18 +1527,13 @@ private void processPegoutsInBatch(
BtcTransaction batchPegoutTransaction = result.getBtcTx();
Keccak256 batchPegoutCreationTxHash = rskTx.getHash();

settleReleaseRequest(pegoutsWaitingForConfirmations, batchPegoutTransaction, batchPegoutCreationTxHash, totalPegoutValue);
settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, batchPegoutTransaction, batchPegoutCreationTxHash, totalPegoutValue);

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

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

adjustBalancesIfChangeOutputWasDust(batchPegoutTransaction, totalPegoutValue, wallet);
}
Expand Down
6 changes: 0 additions & 6 deletions rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@ private static void removeSignaturesFromTransactionWithP2shMultiSigInputs(BtcTra
}
}

public static void addInputFromMatchingOutputScript(BtcTransaction transaction, BtcTransaction sourceTransaction, Script expectedOutputScript) {
List<TransactionOutput> outputs = sourceTransaction.getOutputs();
searchForOutput(outputs, expectedOutputScript)
.ifPresent(transaction::addInput);
}

public static Script createBaseP2SHInputScriptThatSpendsFromRedeemScript(Script redeemScript) {
Script outputScript = ScriptBuilder.createP2SHOutputScript(redeemScript);
return outputScript.createEmptyInputScript(null, redeemScript);
Expand Down
15 changes: 11 additions & 4 deletions rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import static co.rsk.peg.BridgeSupportTestUtil.*;
import static co.rsk.peg.PegUtils.getFlyoverRedeemScript;
import static co.rsk.peg.ReleaseTransactionBuilder.BTC_TX_VERSION_2;
import static co.rsk.peg.bitcoin.BitcoinTestUtils.generateSignerEncodedSignatures;
import static co.rsk.peg.bitcoin.BitcoinTestUtils.generateTransactionInputsSigHashes;
import static co.rsk.peg.bitcoin.BitcoinTestUtils.*;
import static co.rsk.peg.bitcoin.BitcoinUtils.*;
import static co.rsk.peg.bitcoin.UtxoUtils.extractOutpointValues;
import static co.rsk.peg.federation.FederationStorageIndexKey.NEW_FEDERATION_BTC_UTXOS_KEY;
Expand Down Expand Up @@ -291,13 +290,17 @@ void updateCollections_whenThereAreNoEnoughUTXOs_shouldNotCreateFundTransaction(

@Test
void updateCollections_whenFundTxCanBeCreated_createsExpectedFundTxAndSavesTheHashInStorageEntryAndPerformsPegoutActions() throws Exception {
// arrange
int activeFederationUtxosSizeBeforeCreatingFundTx = federationSupport.getActiveFederationBtcUTXOs().size();

// act
bridgeSupport.updateCollections(rskTx);
bridgeStorageProvider.save();

// assert
Sha256Hash svpFundTxHashUnsigned = assertSvpFundTxHashUnsignedIsInStorage();
assertSvpFundTxReleaseWasSettled(svpFundTxHashUnsigned);
assertActiveFederationUtxosSize(activeFederationUtxosSizeBeforeCreatingFundTx - 1);
assertSvpFundTransactionHasExpectedInputsAndOutputs();
}

Expand Down Expand Up @@ -657,10 +660,14 @@ private void assertSvpSpendTxHasExpectedInputsAndOutputs() {
assertEquals(1, outputs.size());

long calculatedTransactionSize = 1762L; // using calculatePegoutTxSize method
Coin expectedAmount = feePerKb
Coin fees = feePerKb
.multiply(calculatedTransactionSize * 12L / 10L) // back up calculation
.divide(1000);
assertOutputWasSentToExpectedScriptWithExpectedAmount(outputs, activeFederation.getP2SHScript(), expectedAmount);

Coin valueToSend = spendableValueFromProposedFederation
.multiply(2)
.minus(fees);
assertOutputWasSentToExpectedScriptWithExpectedAmount(outputs, activeFederation.getP2SHScript(), valueToSend);
}

private void assertInputsHaveExpectedScriptSig(List<TransactionInput> inputs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package co.rsk.peg.bitcoin;

import static co.rsk.bitcoinj.script.ScriptBuilder.createP2SHOutputScript;
import static co.rsk.peg.bitcoin.BitcoinUtils.extractRedeemScriptFromInput;
import static co.rsk.peg.bitcoin.BitcoinUtils.generateSigHashForP2SHTransactionInput;
import static co.rsk.peg.bitcoin.BitcoinUtils.*;

import co.rsk.bitcoinj.core.*;
import co.rsk.bitcoinj.crypto.TransactionSignature;
Expand Down Expand Up @@ -251,4 +250,10 @@ private static BtcTransaction createCoinbaseTxWithWitnessReservedValue(NetworkPa

return coinbaseTx;
}

public static void addInputFromMatchingOutputScript(BtcTransaction transaction, BtcTransaction sourceTransaction, Script expectedOutputScript) {
List<TransactionOutput> outputs = sourceTransaction.getOutputs();
searchForOutput(outputs, expectedOutputScript)
.ifPresent(transaction::addInput);
}
}
42 changes: 1 addition & 41 deletions rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinUtilsTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.rsk.peg.bitcoin;

import static co.rsk.peg.bitcoin.BitcoinTestUtils.addInputFromMatchingOutputScript;
import static co.rsk.peg.bitcoin.BitcoinUtils.*;
import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -415,47 +416,6 @@ void getTransactionHashWithoutSignatures_forRealPegout() {
assertEquals(hashFromReleaseRequestedEvent, btcTxHashWithoutSigs);
}

@Test
void addInputFromOutputSentToScript_withNoMatchingOutputScript_shouldNotAddInput() {
// arrange
Federation federation = P2shErpFederationBuilder.builder().build();
Script redeemScript = federation.getRedeemScript();
Script outputScript = ScriptBuilder.createP2SHOutputScript(redeemScript);
BtcTransaction sourceTransaction = new BtcTransaction(btcMainnetParams);
sourceTransaction.addOutput(Coin.valueOf(1000L), outputScript);

// act
BtcTransaction newTransaction = new BtcTransaction(btcMainnetParams);

Federation anotherFederation = StandardMultiSigFederationBuilder.builder().build();
Script anotherRedeemScript = anotherFederation.getRedeemScript();
Script anotherOutputScript = ScriptBuilder.createP2SHOutputScript(anotherRedeemScript);
addInputFromMatchingOutputScript(newTransaction, sourceTransaction, anotherOutputScript);

// assert
assertEquals(0, newTransaction.getInputs().size());
}

@Test
void addInputFromOutputSentToScript_withMatchingOutputScript_shouldAddInputWithOutpointFromOutput() {
// arrange
Federation federation = P2shErpFederationBuilder.builder().build();
Script redeemScript = federation.getRedeemScript();
Script outputScript = ScriptBuilder.createP2SHOutputScript(redeemScript);

BtcTransaction sourceTransaction = new BtcTransaction(btcMainnetParams);
sourceTransaction.addOutput(Coin.valueOf(1000L), outputScript);

// act
BtcTransaction newTransaction = new BtcTransaction(btcMainnetParams);
addInputFromMatchingOutputScript(newTransaction, sourceTransaction, outputScript);

// assert
TransactionInput newTransactionInput = newTransaction.getInput(0);
TransactionOutput sourceTransactionOutput = sourceTransaction.getOutput(0);
assertEquals(newTransactionInput.getOutpoint().getHash(), sourceTransactionOutput.getParentTransactionHash());
}

@Test
void createBaseP2SHInputScriptThatSpendsFromRedeemScript_shouldCreateExpectedScriptSig() {
// arrange
Expand Down
Loading