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

Send a legacy PegIn transaction from a Multisig address #2901

Open
wants to merge 7 commits into
base: pegin-it-registerBtcTx-negativeHeight
Choose a base branch
from
177 changes: 146 additions & 31 deletions rskj-core/src/test/java/co/rsk/peg/RegisterBtcTransactionIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import co.rsk.core.RskAddress;
import co.rsk.net.utils.TransactionUtils;
import co.rsk.peg.bitcoin.BitcoinTestUtils;
import co.rsk.peg.bitcoin.UtxoUtils;
import co.rsk.peg.btcLockSender.BtcLockSenderProvider;
import co.rsk.peg.constants.BridgeConstants;
import co.rsk.peg.constants.BridgeMainNetConstants;
Expand All @@ -17,6 +18,7 @@
import co.rsk.peg.feeperkb.FeePerKbStorageProviderImpl;
import co.rsk.peg.feeperkb.FeePerKbSupport;
import co.rsk.peg.feeperkb.FeePerKbSupportImpl;
import co.rsk.peg.pegin.RejectedPeginReason;
import co.rsk.peg.storage.BridgeStorageAccessorImpl;
import co.rsk.peg.storage.StorageAccessor;
import co.rsk.peg.utils.BridgeEventLoggerImpl;
Expand All @@ -43,29 +45,29 @@ class RegisterBtcTransactionIT {
private final Transaction rskTx = TransactionUtils.createTransaction();
private final Coin minimumPeginValue = bridgeConstants.getMinimumPeginTxValue(activations);
private final Block rskExecutionBlock = getRskExecutionBlock(RSK_EXECUTION_BLOCK_NUMBER, RSK_EXECUTION_BLOCK_TIMESTAMP);
private final BtcECKey btcPublicKey = BitcoinTestUtils.getBtcEcKeyFromSeed("seed");
private final Federation federation = P2shErpFederationBuilder.builder().build();
private Repository repository;
private FederationSupport federationSupport;
private BridgeStorageProvider bridgeStorageProvider;
private BtcTransaction bitcoinTransaction;
private PartialMerkleTree pmtWithTransactions;
private int btcBlockWithPmtHeight;
private RskAddress rskReceiver;
private BridgeSupport bridgeSupport;
private ArrayList<LogInfo> logs;
private BtcBlockStoreWithCache btcBlockStoreWithCache;
private BtcLockSenderProvider btcLockSenderProvider;


@BeforeEach
void setUp() throws Exception{
void setUp() {
repository = BridgeSupportTestUtil.createRepository().startTracking();

BtcLockSenderProvider btcLockSenderProvider = new BtcLockSenderProvider();
btcLockSenderProvider = new BtcLockSenderProvider();
StorageAccessor bridgeStorageAccessor = new BridgeStorageAccessorImpl(repository);

FeePerKbStorageProvider feePerKbStorageProvider = new FeePerKbStorageProviderImpl(bridgeStorageAccessor);
FeePerKbSupport feePerKbSupport = new FeePerKbSupportImpl(bridgeConstants.getFeePerKbConstants(), feePerKbStorageProvider);

FederationStorageProvider federationStorageProvider = new FederationStorageProviderImpl(bridgeStorageAccessor);
Federation federation = P2shErpFederationBuilder.builder().build();
federationStorageProvider.setNewFederation(federation);
FederationConstants federationConstants = bridgeConstants.getFederationConstants();
federationSupport = FederationSupportBuilder.builder()
Expand All @@ -76,23 +78,14 @@ void setUp() throws Exception{

bridgeStorageProvider = new BridgeStorageProvider(repository, PrecompiledContracts.BRIDGE_ADDR, btcNetworkParams, activations);
BtcBlockStoreWithCache.Factory btcBlockStoreFactory = new RepositoryBtcBlockStoreWithCache.Factory(btcNetworkParams, 100, 100);
BtcBlockStoreWithCache btcBlockStoreWithCache = btcBlockStoreFactory.newInstance(repository, bridgeConstants, bridgeStorageProvider, activations);
btcBlockStoreWithCache = btcBlockStoreFactory.newInstance(repository, bridgeConstants, bridgeStorageProvider, activations);

BtcECKey btcPublicKey = BitcoinTestUtils.getBtcEcKeyFromSeed("seed");
ECKey ecKey = ECKey.fromPublicOnly(btcPublicKey.getPubKey());
rskReceiver = new RskAddress(ecKey.getAddress());
bitcoinTransaction = createPegInTransaction(federationSupport.getActiveFederation().getAddress(), minimumPeginValue, btcPublicKey);

pmtWithTransactions = createValidPmtForTransactions(List.of(bitcoinTransaction.getHash()), btcNetworkParams);
btcBlockWithPmtHeight = bridgeConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeConstants.getPegoutTxIndexGracePeriodInBtcBlocks();
int chainHeight = btcBlockWithPmtHeight + bridgeConstants.getBtc2RskMinimumAcceptableConfirmations();

logs = new ArrayList<>();
BridgeEventLoggerImpl bridgeEventLogger = new BridgeEventLoggerImpl(bridgeConstants, activations, logs);

recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcNetworkParams);
bridgeStorageProvider.save();

bridgeSupport = bridgeSupportBuilder
.withBridgeConstants(bridgeConstants)
.withProvider(bridgeStorageProvider)
Expand All @@ -105,41 +98,57 @@ void setUp() throws Exception{
.withRepository(repository)
.withBtcLockSenderProvider(btcLockSenderProvider)
.build();
bridgeStorageProvider.save();
}

@Test
void registerBtcTransaction_forALegacyBtcTransaction_shouldRegisterTheNewUtxoAndTransferTheRbtcBalance() throws Exception {
// Arrange
BtcTransaction btcTransaction = createPegInTransaction(federationSupport.getActiveFederation().getAddress(), minimumPeginValue, btcPublicKey);
PartialMerkleTree pmtWithTransactions = createValidPmtForTransactions(List.of(btcTransaction.getHash()), btcNetworkParams);
int btcBlockWithPmtHeight = bridgeConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeConstants.getPegoutTxIndexGracePeriodInBtcBlocks();
int chainHeight = btcBlockWithPmtHeight + bridgeConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcNetworkParams);
bridgeStorageProvider.save();

// Act
bridgeSupport.registerBtcTransaction(rskTx, bitcoinTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.registerBtcTransaction(rskTx, btcTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.save();

// Assert
Optional<Long> heightIfBtcTxHashIsAlreadyProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(bitcoinTransaction.getHash());
Optional<Long> heightIfBtcTxHashIsAlreadyProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(btcTransaction.getHash());
assertTrue(heightIfBtcTxHashIsAlreadyProcessed.isPresent());
assertEquals(RSK_EXECUTION_BLOCK_NUMBER, heightIfBtcTxHashIsAlreadyProcessed.get());

int outputIndex = 0;
TransactionOutput output = bitcoinTransaction.getOutput(outputIndex);
List<UTXO> expectedFederationUtxos = List.of(utxoOf(bitcoinTransaction, output));
TransactionOutput output = btcTransaction.getOutput(outputIndex);
List<UTXO> expectedFederationUtxos = List.of(utxoOf(btcTransaction, output));
assertEquals(expectedFederationUtxos, federationSupport.getActiveFederationBtcUTXOs());

co.rsk.core.Coin expectedReceiverBalance = co.rsk.core.Coin.fromBitcoin(output.getValue());
assertEquals(expectedReceiverBalance, repository.getBalance(rskReceiver));

assertLogPegInBtc();
assertLogPegInBtc(btcTransaction);
}

@Test
void registerBtcTransaction_forARepeatedLegacyBtcTransaction_shouldNotPerformAnyChange() throws Exception {
// Arrange
bridgeSupport.registerBtcTransaction(rskTx, bitcoinTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
BtcTransaction btcTransaction = createPegInTransaction(federationSupport.getActiveFederation().getAddress(), minimumPeginValue, btcPublicKey);
PartialMerkleTree pmtWithTransactions = createValidPmtForTransactions(List.of(btcTransaction.getHash()), btcNetworkParams);
int btcBlockWithPmtHeight = bridgeConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeConstants.getPegoutTxIndexGracePeriodInBtcBlocks();
int chainHeight = btcBlockWithPmtHeight + bridgeConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcNetworkParams);

bridgeStorageProvider.save();
bridgeSupport.registerBtcTransaction(rskTx, btcTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.save();

co.rsk.core.Coin expectedReceiverBalance = repository.getBalance(rskReceiver);
List<UTXO> expectedFederationUTXOs = List.copyOf(federationSupport.getActiveFederationBtcUTXOs());

// Act
bridgeSupport.registerBtcTransaction(rskTx, bitcoinTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.registerBtcTransaction(rskTx, btcTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.save();

// Assert
Expand All @@ -150,27 +159,86 @@ void registerBtcTransaction_forARepeatedLegacyBtcTransaction_shouldNotPerformAny
@Test
void registerBtcTransaction_whenLegacyBtcTransactionWithNegativeHeight_shouldNotPerformAnyChange() throws Exception {
// Arrange
BtcTransaction btcTransaction = createPegInTransaction(federationSupport.getActiveFederation().getAddress(), minimumPeginValue, btcPublicKey);
PartialMerkleTree pmtWithTransactions = createValidPmtForTransactions(List.of(btcTransaction.getHash()), btcNetworkParams);
int btcBlockWithPmtHeight = bridgeConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeConstants.getPegoutTxIndexGracePeriodInBtcBlocks();
int chainHeight = btcBlockWithPmtHeight + bridgeConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcNetworkParams);
bridgeStorageProvider.save();

co.rsk.core.Coin expectedReceiverBalance = repository.getBalance(rskReceiver);
List<UTXO> expectedFederationUTXOs = List.copyOf(federationSupport.getActiveFederationBtcUTXOs());

// Act
int height = -1;
bridgeSupport.registerBtcTransaction(rskTx, bitcoinTransaction.bitcoinSerialize(), height, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.registerBtcTransaction(rskTx, btcTransaction.bitcoinSerialize(), height, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.save();

// Assert
assertEquals(expectedFederationUTXOs, federationSupport.getActiveFederationBtcUTXOs());
assertEquals(expectedReceiverBalance, repository.getBalance(rskReceiver));
}

@Test
void registerBtcTransaction_whenLegacyPeginBtcTransactionFromAMultiSig_shouldRefundTheFunds() throws Exception {
// Arrange
BtcTransaction btcTransaction = createMultiSigPegInTransaction(federationSupport.getActiveFederation().getAddress(), minimumPeginValue);
PartialMerkleTree pmtWithTransactions = createValidPmtForTransactions(List.of(btcTransaction.getHash()), btcNetworkParams);
int btcBlockWithPmtHeight = bridgeConstants.getBtcHeightWhenPegoutTxIndexActivates() + bridgeConstants.getPegoutTxIndexGracePeriodInBtcBlocks();
int chainHeight = btcBlockWithPmtHeight + bridgeConstants.getBtc2RskMinimumAcceptableConfirmations();
recreateChainFromPmt(btcBlockStoreWithCache, chainHeight, pmtWithTransactions, btcBlockWithPmtHeight, btcNetworkParams);
bridgeSupport.save();

co.rsk.core.Coin expectedReceiverBalance = repository.getBalance(rskReceiver);
List<UTXO> expectedFederationUTXOs = List.copyOf(federationSupport.getActiveFederationBtcUTXOs());

// Act
bridgeSupport.registerBtcTransaction(rskTx, btcTransaction.bitcoinSerialize(), btcBlockWithPmtHeight, pmtWithTransactions.bitcoinSerialize());
bridgeSupport.save();

// Assert
Set<PegoutsWaitingForConfirmations.Entry> pegoutEntries = bridgeStorageProvider.getPegoutsWaitingForConfirmations().getEntries();
int expectedPegoutsWaitingForConfirmations = 1;
assertEquals(expectedPegoutsWaitingForConfirmations, pegoutEntries.size());

Optional<PegoutsWaitingForConfirmations.Entry> pegOutWaitingForConfirmationOptional = pegoutEntries.stream().findFirst();
assertTrue(pegOutWaitingForConfirmationOptional.isPresent());
Comment on lines +200 to +205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do the findFirst or size strategy for both of them. For consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean, I use both approaches to say: that it's the only one and it's exactly the one I am expecting

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, the size assertion indirectly ensures that there is exactly one pegout entry, making the second assertion somewhat redundant. Not a big deal, but you can simplify the code to have a single assert while maintaining clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I always wonder whether asserting an isPresent with an optional makes sense. The list of PegoutsWaitingForConfirmations.Entry may have a list of empty optionals

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you have checked that it has a size 1 it is guaranteed that the Optional is present.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List.of(Optional.empty()).size() is equal to 1, that's why I check both


PegoutsWaitingForConfirmations.Entry pegOutWaitingForConfirmationEntry = pegOutWaitingForConfirmationOptional.get();
assertEquals(rskTx.getHash(), pegOutWaitingForConfirmationEntry.getPegoutCreationRskTxHash());
assertEquals(rskExecutionBlock.getNumber() ,pegOutWaitingForConfirmationEntry.getPegoutCreationRskBlockNumber());

// Pegout value + fee == Pegin value
BtcTransaction pegOut = pegOutWaitingForConfirmationEntry.getBtcTransaction();
int outputIndex = 0;
TransactionOutput pegOutOutput = pegOut.getOutput(outputIndex);
Coin pegOutTotalValue = pegOutOutput.getValue().add(pegOut.getFee());
assertEquals(minimumPeginValue, pegOutTotalValue);

Address pegInTxSender = btcLockSenderProvider.tryGetBtcLockSender(btcTransaction).get().getBTCAddress();
Address pegoutReceiver = pegOutOutput.getAddressFromP2SH(btcNetworkParams);
assertEquals(pegInTxSender, pegoutReceiver);

assertRejectedPeginTransaction(btcTransaction);
assertReleaseBtcRequested(rskTx.getHash().getBytes(), pegOut, minimumPeginValue);
assertPegoutTransactionCreated(pegOut.getHash(), UtxoUtils.extractOutpointValues(pegOut));

assertEquals(expectedFederationUTXOs, federationSupport.getActiveFederationBtcUTXOs());
assertEquals(expectedReceiverBalance, repository.getBalance(rskReceiver));

Optional<Long> heightIfBtcTxHashIsAlreadyProcessed = bridgeStorageProvider.getHeightIfBtcTxhashIsAlreadyProcessed(btcTransaction.getHash());
assertTrue(heightIfBtcTxHashIsAlreadyProcessed.isPresent());
assertEquals(RSK_EXECUTION_BLOCK_NUMBER, heightIfBtcTxHashIsAlreadyProcessed.get());
}

private static UTXO utxoOf(BtcTransaction bitcoinTransaction, TransactionOutput output) {
private static UTXO utxoOf(BtcTransaction btcTransaction, TransactionOutput output) {
int height = 0;
return new UTXO(
bitcoinTransaction.getHash(),
btcTransaction.getHash(),
output.getIndex(),
output.getValue(),
height,
bitcoinTransaction.isCoinBase(),
btcTransaction.isCoinBase(),
output.getScriptPubKey()
);
}
Expand All @@ -185,11 +253,58 @@ private BtcTransaction createPegInTransaction(Address federationAddress, Coin co
return btcTx;
}

private void assertLogPegInBtc() {
Sha256Hash peginTransactionHash = bitcoinTransaction.getHash();
List<DataWord> encodedTopics = getEncodedTopics(BridgeEvents.PEGIN_BTC.getEvent(), rskReceiver.toString(), peginTransactionHash.getBytes());
private BtcTransaction createMultiSigPegInTransaction(Address federationAddress, Coin coin) {
BtcTransaction btcTx = new BtcTransaction(btcNetworkParams);

btcTx.addInput(
BitcoinTestUtils.createHash(1),
0,
ScriptBuilder.createP2SHMultiSigInputScript(null, federationSupport.getActiveFederation().getRedeemScript())
);
btcTx.addOutput(new TransactionOutput(btcNetworkParams, btcTx, coin, federationAddress));

return btcTx;
}

private void assertLogPegInBtc(BtcTransaction btcTransaction) {
CallTransaction.Function pegInBtcEvent = BridgeEvents.PEGIN_BTC.getEvent();
Sha256Hash peginTransactionHash = btcTransaction.getHash();

List<DataWord> encodedTopics = getEncodedTopics(pegInBtcEvent, rskReceiver.toString(), peginTransactionHash.getBytes());

int protocolVersion = 0;
byte[] encodedData = getEncodedData(BridgeEvents.PEGIN_BTC.getEvent(), minimumPeginValue.getValue(), protocolVersion);
byte[] encodedData = getEncodedData(pegInBtcEvent, minimumPeginValue.getValue(), protocolVersion);

assertEventWasEmittedWithExpectedTopics(encodedTopics, logs);
assertEventWasEmittedWithExpectedData(encodedData, logs);
}

private void assertRejectedPeginTransaction(BtcTransaction btcTransaction) {
CallTransaction.Function rejectedPeginEvent = BridgeEvents.REJECTED_PEGIN.getEvent();
Sha256Hash peginTransactionHash = btcTransaction.getHash();
List<DataWord> encodedTopics = getEncodedTopics(rejectedPeginEvent, peginTransactionHash.getBytes());
byte[] encodedData = getEncodedData(rejectedPeginEvent, RejectedPeginReason.LEGACY_PEGIN_MULTISIG_SENDER.getValue());

assertEventWasEmittedWithExpectedTopics(encodedTopics, logs);
assertEventWasEmittedWithExpectedData(encodedData, logs);
}

private void assertReleaseBtcRequested(byte[] rskTransactionHash, BtcTransaction pegoutTransaction, Coin amount) {
CallTransaction.Function rejectedPeginEvent = BridgeEvents.RELEASE_REQUESTED.getEvent();
byte[] pegoutTransactionHash = pegoutTransaction.getHash().getBytes();
List<DataWord> encodedTopics = getEncodedTopics(rejectedPeginEvent, rskTransactionHash, pegoutTransactionHash);
byte[] encodedData = getEncodedData(rejectedPeginEvent, amount.getValue());

assertEventWasEmittedWithExpectedTopics(encodedTopics, logs);
assertEventWasEmittedWithExpectedData(encodedData, logs);
}

private void assertPegoutTransactionCreated(Sha256Hash pegoutTransactionHash, List<Coin> outpointValues) {
CallTransaction.Function pegoutTransactionCreatedEvent = BridgeEvents.PEGOUT_TRANSACTION_CREATED.getEvent();

List<DataWord> encodedTopics = getEncodedTopics(pegoutTransactionCreatedEvent, pegoutTransactionHash.getBytes());
byte[] serializedOutpointValues = UtxoUtils.encodeOutpointValues(outpointValues);
byte[] encodedData = getEncodedData(pegoutTransactionCreatedEvent, serializedOutpointValues);

assertEventWasEmittedWithExpectedTopics(encodedTopics, logs);
assertEventWasEmittedWithExpectedData(encodedData, logs);
Expand Down
Loading