Skip to content

Commit

Permalink
Merge pull request #2851 from rsksmart/register_spend_tx
Browse files Browse the repository at this point in the history
Register spend tx
  • Loading branch information
marcos-iov authored Nov 21, 2024
2 parents 3b02692 + 29bc369 commit 0d4c61c
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 78 deletions.
50 changes: 30 additions & 20 deletions rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ public void registerBtcTransaction(
throw new RegisterBtcTransactionException("Transaction already processed");
}

if (isSvpOngoing() && isTheSvpSpendTransaction(btcTx)) {
registerSvpSpendTransaction(btcTx);
return;
}

PegTxType pegTxType = PegUtils.getTransactionType(
activations,
provider,
Expand All @@ -410,8 +415,8 @@ public void registerBtcTransaction(
case PEGOUT_OR_MIGRATION:
logger.debug("[registerBtcTransaction] This is a peg-out or migration tx {}", btcTx.getHash());
processPegoutOrMigration(btcTx);
if (svpIsOngoing()) {
updateSvpFundTransactionValuesIfPossible(btcTx);
if (isSvpOngoing() && isTheSvpFundTransaction(btcTx)) {
updateSvpFundTransactionValues(btcTx);
}
break;
default:
Expand All @@ -429,21 +434,12 @@ public void registerBtcTransaction(
}
}

private void updateSvpFundTransactionValuesIfPossible(BtcTransaction transaction) {
provider.getSvpFundTxHashUnsigned()
.filter(svpFundTxHashUnsigned -> isTheSvpFundTransaction(svpFundTxHashUnsigned, transaction))
.ifPresent(isTheSvpFundTransaction -> updateSvpFundTransactionValues(transaction));
}

private boolean isTheSvpFundTransaction(Sha256Hash svpFundTransactionHashUnsigned, BtcTransaction transaction) {
Sha256Hash transactionHash = transaction.getHash();

if (!transaction.hasWitness()) {
BtcTransaction transactionCopyWithoutSignatures = new BtcTransaction(networkParameters, transaction.bitcoinSerialize()); // this is needed to not remove signatures from the actual tx
BitcoinUtils.removeSignaturesFromTransactionWithP2shMultiSigInputs(transactionCopyWithoutSignatures);
transactionHash = transactionCopyWithoutSignatures.getHash();
}
return transactionHash.equals(svpFundTransactionHashUnsigned);
private boolean isTheSvpFundTransaction(BtcTransaction transaction) {
return provider.getSvpFundTxHashUnsigned()
.map(svpFundTransactionHashUnsigned ->
getMultiSigTransactionHashWithoutSignatures(networkParameters, transaction).equals(svpFundTransactionHashUnsigned)
)
.orElse(false);
}

private void updateSvpFundTransactionValues(BtcTransaction transaction) {
Expand All @@ -455,6 +451,21 @@ private void updateSvpFundTransactionValues(BtcTransaction transaction) {
provider.setSvpFundTxHashUnsigned(null);
}

private boolean isTheSvpSpendTransaction(BtcTransaction transaction) {
return provider.getSvpSpendTxHashUnsigned()
.filter(svpSpendTransactionHashUnsigned ->
getMultiSigTransactionHashWithoutSignatures(networkParameters, transaction).equals(svpSpendTransactionHashUnsigned)
)
.isPresent();
}

private void registerSvpSpendTransaction(BtcTransaction svpSpendTx) throws IOException {
markTxAsProcessed(svpSpendTx);
saveNewUTXOs(svpSpendTx);
provider.setSvpSpendTxHashUnsigned(null);
// proceed with svp success
}

private Script getLastRetiredFederationP2SHScript() {
return federationSupport.getLastRetiredFederationP2SHScript().orElse(null);
}
Expand Down Expand Up @@ -1082,7 +1093,7 @@ private void allowFederationElectionAgain() {
provider.clearSvpValues();
}

private boolean svpIsOngoing() {
private boolean isSvpOngoing() {
return federationSupport.getProposedFederation()
.filter(this::validationPeriodIsOngoing)
.isPresent();
Expand Down Expand Up @@ -1665,7 +1676,7 @@ public void addSignature(BtcECKey federatorBtcPublicKey, List<byte[]> signatures

Context.propagate(btcContext);

if (svpIsOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) {
if (isSvpOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) {
logger.trace("[addSignature] Going to sign svp spend transaction with federator public key {}", federatorBtcPublicKey);
addSvpSpendTxSignatures(federatorBtcPublicKey, signatures);
return;
Expand Down Expand Up @@ -1760,7 +1771,6 @@ private void addSvpSpendTxSignatures(
provider.getSvpSpendTxWaitingForSignatures()
// The svpSpendTxWFS should always be present at this point, since we already checked isTheSvpSpendTx.
.ifPresent(svpSpendTxWFS -> {

Keccak256 svpSpendTxCreationRskTxHash = svpSpendTxWFS.getKey();
BtcTransaction svpSpendTx = svpSpendTxWFS.getValue();

Expand Down
12 changes: 11 additions & 1 deletion rskj-core/src/main/java/co/rsk/peg/bitcoin/BitcoinUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ public static Optional<Script> extractRedeemScriptFromInput(TransactionInput txI
}
}

public static void removeSignaturesFromTransactionWithP2shMultiSigInputs(BtcTransaction transaction) {
public static Sha256Hash getMultiSigTransactionHashWithoutSignatures(NetworkParameters networkParameters, BtcTransaction transaction) {
if (!transaction.hasWitness()) {
BtcTransaction transactionCopyWithoutSignatures = new BtcTransaction(networkParameters, transaction.bitcoinSerialize()); // this is needed to not remove signatures from the actual tx
BitcoinUtils.removeSignaturesFromTransactionWithP2shMultiSigInputs(transactionCopyWithoutSignatures);
return transactionCopyWithoutSignatures.getHash();
}

return transaction.getHash();
}

private static void removeSignaturesFromTransactionWithP2shMultiSigInputs(BtcTransaction transaction) {
if (transaction.hasWitness()) {
String message = "Removing signatures from SegWit transactions is not allowed.";
logger.error("[removeSignaturesFromTransactionWithP2shMultiSigInputs] {}", message);
Expand Down
187 changes: 146 additions & 41 deletions rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public class BridgeSupportSvpTest {
private Sha256Hash svpSpendTransactionHashUnsigned;
private BtcTransaction svpSpendTransaction;

private PartialMerkleTree pmtWithTransactions;
private int btcBlockWithPmtHeight;

@BeforeEach
void setUp() {
long rskExecutionBlockNumber = 1000L;
Expand Down Expand Up @@ -467,8 +470,6 @@ private void assertSvpFundTransactionValuesWereNotUpdated() {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tag("Fund transaction registration tests")
class FundTxRegistrationTests {
private PartialMerkleTree pmtWithTransactions;
private int btcBlockWithPmtHeight;

@Test
void registerBtcTransaction_forSvpFundTransactionChange_whenProposedFederationDoesNotExist_shouldRegisterTransactionButNotUpdateSvpFundTransactionValues() throws Exception {
Expand Down Expand Up @@ -595,43 +596,6 @@ void registerBtcTransaction_forSvpFundTransactionChange_whenSvpPeriodIsOngoing_s
assertSvpFundTransactionValuesWereUpdated();
}

private void setUpForTransactionRegistration(BtcTransaction transaction) throws BlockStoreException {
// recreate a valid chain that has the tx, so it passes the previous checks in registerBtcTransaction
BtcBlockStoreWithCache.Factory btcBlockStoreFactory = new RepositoryBtcBlockStoreWithCache.Factory(btcMainnetParams, 100, 100);
BtcBlockStoreWithCache btcBlockStoreWithCache = btcBlockStoreFactory.newInstance(repository, bridgeMainNetConstants, bridgeStorageProvider, allActivations);

pmtWithTransactions = createValidPmtForTransactions(Collections.singletonList(transaction.getHash()), btcMainnetParams);
btcBlockWithPmtHeight = bridgeMainNetConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeMainNetConstants.getPegoutTxIndexGracePeriodInBtcBlocks(); // we want pegout tx index to be activated

int chainHeight = btcBlockWithPmtHeight + bridgeMainNetConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcMainnetParams);

bridgeStorageProvider.save();

bridgeSupport = bridgeSupportBuilder
.withBridgeConstants(bridgeMainNetConstants)
.withProvider(bridgeStorageProvider)
.withActivations(allActivations)
.withFederationSupport(federationSupport)
.withFeePerKbSupport(feePerKbSupport)
.withExecutionBlock(rskExecutionBlock)
.withBtcBlockStoreFactory(btcBlockStoreFactory)
.withRepository(repository)
.build();
}

private void assertActiveFederationUtxosSize(int expectedActiveFederationUtxosSize) {
assertEquals(expectedActiveFederationUtxosSize, federationSupport.getActiveFederationBtcUTXOs().size());
}

private void assertTransactionWasProcessed(Sha256Hash transactionHash) throws IOException {
Optional<Long> rskBlockHeightAtWhichBtcTxWasProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(transactionHash);
assertTrue(rskBlockHeightAtWhichBtcTxWasProcessed.isPresent());

long rskExecutionBlockNumber = rskExecutionBlock.getNumber();
assertEquals(rskExecutionBlockNumber, rskBlockHeightAtWhichBtcTxWasProcessed.get());
}

private void assertSvpFundTransactionValuesWereUpdated() {
Optional<BtcTransaction> svpFundTransactionSignedOpt = bridgeStorageProvider.getSvpFundTxSigned();
assertTrue(svpFundTransactionSignedOpt.isPresent());
Expand Down Expand Up @@ -666,7 +630,7 @@ void updateCollections_whenSpendTxCanBeCreated_createsExpectedSpendTxAndSavesThe
bridgeStorageProvider.save();

// assert
svpSpendTransactionHashUnsigned = assertSvpSpendTxHashUnsignedWasSavedInStorage();
svpSpendTransactionHashUnsigned = assertSvpSpendTxHashUnsignedIsSavedInStorage();
svpSpendTransaction = assertSvpSpendTxIsWaitingForSignatures();
assertSvpSpendTxHasExpectedInputsAndOutputs();

Expand Down Expand Up @@ -727,7 +691,7 @@ private void assertInputHasExpectedScriptSig(TransactionInput input, Script rede
}
}

private Sha256Hash assertSvpSpendTxHashUnsignedWasSavedInStorage() {
private Sha256Hash assertSvpSpendTxHashUnsignedIsSavedInStorage() {
Optional<Sha256Hash> svpSpendTransactionHashUnsignedOpt = bridgeStorageProvider.getSvpSpendTxHashUnsigned();
assertTrue(svpSpendTransactionHashUnsignedOpt.isPresent());

Expand Down Expand Up @@ -982,6 +946,105 @@ private void assertFederatorSignedInputs(List<TransactionInput> inputs, List<Sha
}
}

@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tag("Spend transaction registration tests")
class SpendTxRegistrationTests {
@Test
void registerBtcTransaction_whenValidationPeriodEnded_shouldNotProcessSpendTx() throws BlockStoreException, BridgeIllegalArgumentException, IOException {
// arrange
arrangeExecutionBlockIsAfterValidationPeriodEnded();

arrangeSvpSpendTransaction();
setUpForTransactionRegistration(svpSpendTransaction);

// act
int activeFederationUtxosSizeBeforeRegisteringTx = federationSupport.getActiveFederationBtcUTXOs().size();
bridgeSupport.registerBtcTransaction(
rskTx,
svpSpendTransaction.bitcoinSerialize(),
btcBlockWithPmtHeight,
pmtWithTransactions.bitcoinSerialize()
);
bridgeStorageProvider.save();

// assert
assertActiveFederationUtxosSize(activeFederationUtxosSizeBeforeRegisteringTx);
assertTransactionWasNotProcessed(svpSpendTransaction.getHash());
assertSvpSpendTxHashUnsignedIsSavedInStorage();
}

@Test
void registerBtcTransaction_whenIsTheSpendTransaction_shouldProcessSpendTx() throws BlockStoreException, BridgeIllegalArgumentException, IOException {
// arrange
arrangeSvpSpendTransaction();
setUpForTransactionRegistration(svpSpendTransaction);

// act
int activeFederationUtxosSizeBeforeRegisteringTx = federationSupport.getActiveFederationBtcUTXOs().size();
bridgeSupport.registerBtcTransaction(
rskTx,
svpSpendTransaction.bitcoinSerialize(),
btcBlockWithPmtHeight,
pmtWithTransactions.bitcoinSerialize()
);
bridgeStorageProvider.save();

// assert
assertActiveFederationUtxosSize(activeFederationUtxosSizeBeforeRegisteringTx + 1);
assertTransactionWasProcessed(svpSpendTransaction.getHash());
assertNoSvpSpendTxHash();
}

@Test
void registerBtcTransaction_whenIsNotTheSpendTransaction_shouldNotProcessSpendTx() throws BlockStoreException, BridgeIllegalArgumentException, IOException {
// arrange
arrangeSvpSpendTransaction();
setUpForTransactionRegistration(svpSpendTransaction);

BtcTransaction pegout = createPegout(proposedFederation.getRedeemScript());
savePegoutIndex(pegout);
signInputs(pegout);
setUpForTransactionRegistration(pegout);

// act
int activeFederationUtxosSizeBeforeRegisteringTx = federationSupport.getActiveFederationBtcUTXOs().size();
bridgeSupport.registerBtcTransaction(
rskTx,
pegout.bitcoinSerialize(),
btcBlockWithPmtHeight,
pmtWithTransactions.bitcoinSerialize()
);
bridgeStorageProvider.save();

// assert
assertActiveFederationUtxosSize(activeFederationUtxosSizeBeforeRegisteringTx + 1);
assertTransactionWasNotProcessed(svpSpendTransaction.getHash());
assertSvpSpendTxHashUnsignedIsSavedInStorage();
}

@Test
void registerBtcTransaction_whenSpendTransactionHashIsNotSaved_shouldNotProcessSpendTx() throws BlockStoreException, BridgeIllegalArgumentException, IOException {
// arrange
recreateSvpSpendTransactionUnsigned();
setUpForTransactionRegistration(svpSpendTransaction);

// act
int activeFederationUtxosSizeBeforeRegisteringTx = federationSupport.getActiveFederationBtcUTXOs().size();
bridgeSupport.registerBtcTransaction(
rskTx,
svpSpendTransaction.bitcoinSerialize(),
btcBlockWithPmtHeight,
pmtWithTransactions.bitcoinSerialize()
);
bridgeStorageProvider.save();

// assert
assertActiveFederationUtxosSize(activeFederationUtxosSizeBeforeRegisteringTx);
assertTransactionWasNotProcessed(svpSpendTransaction.getHash());
}
}

private void arrangeExecutionBlockIsAfterValidationPeriodEnded() {
long validationPeriodEndBlock = proposedFederation.getCreationBlockNumber()
+ bridgeMainNetConstants.getFederationConstants().getValidationPeriodDurationInBlocks();
Expand Down Expand Up @@ -1081,6 +1144,48 @@ private void saveSvpSpendTransactionValues() {
bridgeStorageProvider.save();
}

private void setUpForTransactionRegistration(BtcTransaction transaction) throws BlockStoreException {
// recreate a valid chain that has the tx, so it passes the previous checks in registerBtcTransaction
BtcBlockStoreWithCache.Factory btcBlockStoreFactory = new RepositoryBtcBlockStoreWithCache.Factory(btcMainnetParams, 100, 100);
BtcBlockStoreWithCache btcBlockStoreWithCache = btcBlockStoreFactory.newInstance(repository, bridgeMainNetConstants, bridgeStorageProvider, allActivations);

pmtWithTransactions = createValidPmtForTransactions(Collections.singletonList(transaction.getHash()), btcMainnetParams);
btcBlockWithPmtHeight = bridgeMainNetConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeMainNetConstants.getPegoutTxIndexGracePeriodInBtcBlocks(); // we want pegout tx index to be activated

int chainHeight = btcBlockWithPmtHeight + bridgeMainNetConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcMainnetParams);

bridgeStorageProvider.save();

bridgeSupport = bridgeSupportBuilder
.withBridgeConstants(bridgeMainNetConstants)
.withProvider(bridgeStorageProvider)
.withActivations(allActivations)
.withFederationSupport(federationSupport)
.withFeePerKbSupport(feePerKbSupport)
.withExecutionBlock(rskExecutionBlock)
.withBtcBlockStoreFactory(btcBlockStoreFactory)
.withRepository(repository)
.build();
}

private void assertActiveFederationUtxosSize(int expectedActiveFederationUtxosSize) {
assertEquals(expectedActiveFederationUtxosSize, federationSupport.getActiveFederationBtcUTXOs().size());
}

private void assertTransactionWasProcessed(Sha256Hash transactionHash) throws IOException {
Optional<Long> rskBlockHeightAtWhichBtcTxWasProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(transactionHash);
assertTrue(rskBlockHeightAtWhichBtcTxWasProcessed.isPresent());

long rskExecutionBlockNumber = rskExecutionBlock.getNumber();
assertEquals(rskExecutionBlockNumber, rskBlockHeightAtWhichBtcTxWasProcessed.get());
}

private void assertTransactionWasNotProcessed(Sha256Hash transactionHash) throws IOException {
Optional<Long> rskBlockHeightAtWhichBtcTxWasProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(transactionHash);
assertFalse(rskBlockHeightAtWhichBtcTxWasProcessed.isPresent());
}

private void assertNoSvpFundTxHashUnsigned() {
assertFalse(bridgeStorageProvider.getSvpFundTxHashUnsigned().isPresent());
}
Expand Down
Loading

0 comments on commit 0d4c61c

Please sign in to comment.