diff --git a/rskj-core/src/test/java/co/rsk/peg/MinimumPegValueTest.java b/rskj-core/src/test/java/co/rsk/peg/MinimumPegValueTest.java new file mode 100644 index 00000000000..24040f8d2ae --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/peg/MinimumPegValueTest.java @@ -0,0 +1,300 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.peg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import co.rsk.bitcoinj.core.Address; +import co.rsk.bitcoinj.core.BtcECKey; +import co.rsk.bitcoinj.core.Coin; +import co.rsk.bitcoinj.core.Context; +import co.rsk.bitcoinj.core.NetworkParameters; +import co.rsk.bitcoinj.core.Sha256Hash; +import co.rsk.bitcoinj.core.UTXO; +import co.rsk.bitcoinj.wallet.Wallet; +import co.rsk.db.MutableTrieCache; +import co.rsk.db.MutableTrieImpl; +import co.rsk.peg.constants.BridgeConstants; +import co.rsk.peg.constants.BridgeMainNetConstants; +import co.rsk.peg.federation.*; +import co.rsk.peg.feeperkb.FeePerKbSupport; +import co.rsk.peg.feeperkb.FeePerKbSupportImpl; +import co.rsk.peg.storage.BridgeStorageAccessorImpl; +import co.rsk.peg.storage.StorageAccessor; +import co.rsk.peg.utils.BridgeEventLogger; +import co.rsk.peg.utils.BridgeEventLoggerImpl; +import co.rsk.test.builders.BridgeSupportBuilder; +import co.rsk.test.builders.FederationSupportBuilder; +import co.rsk.trie.Trie; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.bouncycastle.util.encoders.Hex; +import org.ethereum.config.Constants; +import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.config.blockchain.upgrades.ActivationConfigsForTest; +import org.ethereum.core.BlockTxSignatureCache; +import org.ethereum.core.ReceivedTxSignatureCache; +import org.ethereum.core.Repository; +import org.ethereum.core.SignatureCache; +import org.ethereum.core.Transaction; +import org.ethereum.crypto.ECKey; +import org.ethereum.db.MutableRepository; +import org.ethereum.vm.LogInfo; +import org.ethereum.vm.PrecompiledContracts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MinimumPegValueTest { + + private static final BigInteger NONCE = new BigInteger("0"); + private static final BigInteger GAS_PRICE = new BigInteger("100"); + private static final BigInteger GAS_LIMIT = new BigInteger("1000"); + private static final String DATA = "80af2871"; + private static final ECKey SENDER = new ECKey(); + + private ActivationConfig.ForBlock activations; + private NetworkParameters networkParameters; + private BridgeConstants bridgeMainNetConstants; + private Federation federation; + + @BeforeEach + void setup() { + activations = ActivationConfigsForTest.all().forBlock(0L); + bridgeMainNetConstants = BridgeMainNetConstants.getInstance(); + networkParameters = bridgeMainNetConstants.getBtcParams(); + federation = new P2shErpFederationBuilder().withNetworkParameters(networkParameters).build(); + } + + @ParameterizedTest() + @MethodSource("providePegMinimumParameters") + void whenOneInputofMinPeginValueAndOneOutputOfMinPegoutValue_shouldBuildTransactionSuccessfully( + Coin feePerKb, Coin minimumPeginValue, Coin minimumPegoutValue) { + // list of peg-out requests with the given minimum peg-out value + List entries = Arrays.asList( + createTestEntry(1000, minimumPegoutValue)); + // list of utxos that contains one utxo with the minimum peg-in value + List utxos = Arrays.asList( + new UTXO(getUTXOHash("utxo"), 0, minimumPeginValue, 0, false, federation.getP2SHScript())); + // the federation wallet, with flyover support + Wallet fedWallet = BridgeUtils.getFederationSpendWallet( + new Context(networkParameters), + federation, + utxos, + true, + mock(BridgeStorageProvider.class)); + // build release transaction builder for current fed + ReleaseTransactionBuilder releaseTransactionBuilder = new ReleaseTransactionBuilder( + networkParameters, + fedWallet, + federation.getAddress(), + feePerKb, + activations); + + // build batch peg-out transaction + ReleaseTransactionBuilder.BuildResult result = releaseTransactionBuilder.buildBatchedPegouts(entries); + + // the proposed feePerKb and peg values are compatible + assertEquals(ReleaseTransactionBuilder.Response.SUCCESS, result.getResponseCode()); + } + + @ParameterizedTest() + @MethodSource("providePegMinimumParameters") + void whenOneInputOfTenTimesMinPeginValueAndTenOutputsOfMinPegoutValue_shouldBuildTransactionSuccessfully( + Coin feePerKb, Coin minimumPeginValue, Coin minimumPegoutValue) { + // list of peg-out requests with the given minimum peg-out value + List entries = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + entries.add(createTestEntry(1000 + i, minimumPegoutValue)); + } + // list of utxos that contains one utxo with ten times the minimum peg-in value + List utxos = Arrays.asList( + new UTXO(getUTXOHash("utxo"), 0, minimumPeginValue.times(10), 0, false, federation.getP2SHScript())); + // the federation wallet, with flyover support + Wallet fedWallet = BridgeUtils.getFederationSpendWallet( + new Context(networkParameters), + federation, + utxos, + true, + mock(BridgeStorageProvider.class)); + // build release transaction builder for current fed + ReleaseTransactionBuilder releaseTransactionBuilder = new ReleaseTransactionBuilder( + networkParameters, + fedWallet, + federation.getAddress(), + feePerKb, + activations); + + // build batch peg-out transaction + ReleaseTransactionBuilder.BuildResult result = releaseTransactionBuilder.buildBatchedPegouts(entries); + + // the proposed feePerKb and peg values are compatible + assertEquals(ReleaseTransactionBuilder.Response.SUCCESS, result.getResponseCode()); + } + + @ParameterizedTest() + @MethodSource("providePegMinimumParameters") + void whenTenInputOfMinPeginValueAndTenOutputsOfMinPegoutValue_shouldBuildTransactionSuccessfully( + Coin feePerKb, Coin minimumPeginValue, Coin minimumPegoutValue) { + // list of peg-out requests with the given minimum peg-out value + List entries = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + entries.add(createTestEntry(1000 + i, minimumPegoutValue)); + } + // list of utxos that contains ten utxos with the minimum peg-in value + List utxos = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + utxos.add( + new UTXO(getUTXOHash("utxo" + i), 0, minimumPeginValue, 0, false, federation.getP2SHScript())); + } + // the federation wallet, with flyover support + Wallet fedWallet = BridgeUtils.getFederationSpendWallet( + new Context(networkParameters), + federation, + utxos, + true, + mock(BridgeStorageProvider.class)); + // build release transaction builder for current fed + ReleaseTransactionBuilder releaseTransactionBuilder = new ReleaseTransactionBuilder( + networkParameters, + fedWallet, + federation.getAddress(), + feePerKb, + activations); + + // build batch peg-out transaction + ReleaseTransactionBuilder.BuildResult result = releaseTransactionBuilder.buildBatchedPegouts(entries); + + // the proposed feePerKb and peg values are compatible + assertEquals(ReleaseTransactionBuilder.Response.SUCCESS, result.getResponseCode()); + } + + @ParameterizedTest() + @MethodSource("providePegMinimumParameters") + void whenReleaseBtcCalledWithMinimumPegoutValue_shouldLogReleaseBtcRequestReceived( + Coin feePerKb, Coin minimumPeginValue, Coin minimumPegoutValue) throws IOException { + BridgeConstants bridgeConstants = mock(BridgeConstants.class); + when(bridgeConstants.getBtcParams()).thenReturn(bridgeMainNetConstants.getBtcParams()); + when(bridgeConstants.getMinimumPegoutValuePercentageToReceiveAfterFee()) + .thenReturn(bridgeMainNetConstants.getMinimumPegoutValuePercentageToReceiveAfterFee()); + when(bridgeConstants.getMinimumPegoutTxValue()).thenReturn(minimumPegoutValue); + + List logInfo = new ArrayList<>(); + SignatureCache signatureCache = new BlockTxSignatureCache(new ReceivedTxSignatureCache()); + BridgeEventLoggerImpl eventLogger = spy(new BridgeEventLoggerImpl( + bridgeConstants, activations, logInfo, signatureCache)); + + FeePerKbSupport feePerKbSupport = mock(FeePerKbSupportImpl.class); + when(feePerKbSupport.getFeePerKb()).thenReturn(feePerKb); + + BridgeSupport bridgeSupport = initBridgeSupport( + bridgeConstants, eventLogger, activations, signatureCache, feePerKbSupport); + + bridgeSupport.releaseBtc(buildReleaseRskTx(minimumPegoutValue)); + + verify(eventLogger).logReleaseBtcRequestReceived(any(), any(), any()); + } + + private static Stream providePegMinimumParameters() { + return Stream.of( + // 0.001 BTC - 0.0008 BTC: current feePerKb value + Arguments.of(Coin.valueOf(24_000L), Coin.valueOf(100_000L), Coin.valueOf(80_000L)), + // 0.001 BTC - 0.0008 BTC: maximum feePerKb value that allows all cases to + // succeed + Arguments.of(Coin.valueOf(24_750L), Coin.valueOf(100_000L), Coin.valueOf(80_000L)), + + // 0.0025 BTC - 0.002 BTC: current feePerKb value + Arguments.of(Coin.valueOf(24_000L), Coin.valueOf(250_000L), Coin.valueOf(200_000L)), + // 0.0025 BTC - 0.002 BTC: maximum feePerKb value that allows all cases to + // succeed + Arguments.of(Coin.valueOf(61_750L), Coin.valueOf(250_000L), Coin.valueOf(200_000L))); + } + + /********************************** + * ------- UTILS ------- * + *********************************/ + + private Address getAddress(int pk) { + return BtcECKey.fromPrivate(BigInteger.valueOf(pk)) + .toAddress(NetworkParameters.fromID(NetworkParameters.ID_MAINNET)); + } + + private Sha256Hash getUTXOHash(String generator) { + return Sha256Hash.of(generator.getBytes(StandardCharsets.UTF_8)); + } + + private ReleaseRequestQueue.Entry createTestEntry(int addressPk, Coin amount) { + return new ReleaseRequestQueue.Entry(getAddress(addressPk), amount); + } + + private BridgeSupport initBridgeSupport(BridgeConstants bridgeConstants, BridgeEventLogger eventLogger, + ActivationConfig.ForBlock activations, SignatureCache signatureCache, FeePerKbSupport feePerKbSupport) { + Repository repository = new MutableRepository(new MutableTrieCache(new MutableTrieImpl(null, new Trie()))); + + StorageAccessor bridgeStorageAccessor = new BridgeStorageAccessorImpl(repository); + FederationStorageProvider federationStorageProvider = new FederationStorageProviderImpl(bridgeStorageAccessor); + UTXO utxo = new UTXO(getUTXOHash("utxo"), 0, Coin.COIN.multiply(2), 1, false, federation.getP2SHScript()); + federationStorageProvider.getNewFederationBtcUTXOs(networkParameters, activations).add(utxo); + federationStorageProvider.setNewFederation(federation); + + FederationSupport federationSupport = new FederationSupportBuilder() + .withFederationConstants(bridgeConstants.getFederationConstants()) + .withFederationStorageProvider(federationStorageProvider) + .build(); + + BridgeStorageProvider provider = new BridgeStorageProvider(repository, PrecompiledContracts.BRIDGE_ADDR, + networkParameters, activations); + + return new BridgeSupportBuilder() + .withBridgeConstants(bridgeConstants) + .withProvider(provider) + .withRepository(repository) + .withEventLogger(eventLogger) + .withActivations(activations) + .withSignatureCache(signatureCache) + .withFederationSupport(federationSupport) + .withFeePerKbSupport(feePerKbSupport) + .build(); + } + + private Transaction buildReleaseRskTx(Coin coin) { + Transaction releaseTx = Transaction + .builder() + .nonce(NONCE) + .gasPrice(GAS_PRICE) + .gasLimit(GAS_LIMIT) + .destination(PrecompiledContracts.BRIDGE_ADDR.toHexString()) + .data(Hex.decode(DATA)) + .chainId(Constants.MAINNET_CHAIN_ID) + .value(co.rsk.core.Coin.fromBitcoin(coin).asBigInteger()) + .build(); + releaseTx.sign(SENDER.getPrivKeyBytes()); + return releaseTx; + } +}