diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java index 1f573fa4c4..dc3e231c47 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java @@ -31,6 +31,8 @@ import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.core.Repository; import org.ethereum.vm.DataWord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; /** @@ -40,6 +42,7 @@ * @author Oscar Guindzberg */ public class BridgeStorageProvider { + private static final Logger logger = LoggerFactory.getLogger(BridgeStorageProvider.class); // Dummy value to use when saving key only indexes private static final byte TRUE_VALUE = (byte) 1; @@ -679,6 +682,34 @@ private void saveSvpSpendTxWaitingForSignatures() { ); } + public void clearSvpValues() { + if (svpFundTxHashUnsigned != null) { + logger.warn("[clearSvpValues] Clearing fund tx hash unsigned {} value.", svpFundTxHashUnsigned); + setSvpFundTxHashUnsigned(null); + } + + if (svpFundTxSigned != null) { + logger.warn("[clearSvpValues] Clearing fund tx signed {} value.", svpFundTxSigned.getHash()); + setSvpFundTxSigned(null); + } + + if (svpSpendTxWaitingForSignatures != null) { + Keccak256 rskCreationHash = svpSpendTxWaitingForSignatures.getKey(); + BtcTransaction svpSpendTx = svpSpendTxWaitingForSignatures.getValue(); + logger.warn( + "[clearSvpValues] Clearing spend tx waiting for signatures with spend tx {} and rsk creation hash {} value.", + svpSpendTx.getHash(), rskCreationHash + ); + setSvpSpendTxWaitingForSignatures(null); + setSvpSpendTxHashUnsigned(null); + } + + if (svpSpendTxHashUnsigned != null) { + logger.warn("[clearSvpValues] Clearing spend tx hash unsigned {} value.", svpSpendTxHashUnsigned); + setSvpSpendTxHashUnsigned(null); + } + } + public void save() { saveBtcTxHashesAlreadyProcessed(); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java index 684df2ecb2..55b4e4fa1c 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java @@ -1017,6 +1017,21 @@ private void logUpdateCollections(Transaction rskTx) { eventLogger.logUpdateCollections(sender); } + protected void processSVPFailure(Federation proposedFederation) { + eventLogger.logCommitFederationFailure(rskExecutionBlock, proposedFederation); + logger.warn( + "[processSVPFailure] Proposed federation validation failed at block {}, so federation election will be allowed again.", + rskExecutionBlock.getNumber() + ); + + allowFederationElectionAgain(); + } + + private void allowFederationElectionAgain() { + federationSupport.clearProposedFederation(); + provider.clearSvpValues(); + } + private boolean svpIsOngoing() { return federationSupport.getProposedFederation() .map(Federation::getCreationBlockNumber) diff --git a/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupport.java b/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupport.java index d6e8ca7522..e4c783fd1d 100644 --- a/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupport.java @@ -125,6 +125,8 @@ public interface FederationSupport { */ Optional getProposedFederatorPublicKeyOfType(int index, FederationMember.KeyType keyType); + void clearProposedFederation(); + int voteFederationChange( Transaction tx, ABICallSpec callSpec, diff --git a/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupportImpl.java b/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupportImpl.java index ba1aa28f49..d17fd155cc 100644 --- a/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupportImpl.java +++ b/rskj-core/src/main/java/co/rsk/peg/federation/FederationSupportImpl.java @@ -398,6 +398,11 @@ public Optional getProposedFederationCreationTime() { .map(Federation::getCreationTime); } + @Override + public void clearProposedFederation() { + provider.setProposedFederation(null); + } + @Override public int voteFederationChange(Transaction tx, ABICallSpec callSpec, SignatureCache signatureCache, BridgeEventLogger eventLogger) { String calledFunction = callSpec.getFunction(); diff --git a/rskj-core/src/test/java/co/rsk/peg/BridgeStorageProviderTest.java b/rskj-core/src/test/java/co/rsk/peg/BridgeStorageProviderTest.java index 6fdf7e7d00..bb7f684fcd 100644 --- a/rskj-core/src/test/java/co/rsk/peg/BridgeStorageProviderTest.java +++ b/rskj-core/src/test/java/co/rsk/peg/BridgeStorageProviderTest.java @@ -1162,6 +1162,82 @@ void getSvpSpendTxWaitingForSignatures_whenEntryIsCached_shouldReturnTheCachedEn } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Tag("clear svp values tests") + class ClearSvpValuesTests { + private BridgeStorageProvider bridgeStorageProvider; + + @BeforeEach + void setup() { + Repository repository = createRepository(); + bridgeStorageProvider = createBridgeStorageProvider(repository, mainnetBtcParams, activationsAllForks); + } + + @Test + void clearSvpValues_whenFundTxHashUnsigned_shouldClearValue() { + // arrange + Sha256Hash svpFundTxHashUnsigned = BitcoinTestUtils.createHash(1); + bridgeStorageProvider.setSvpFundTxHashUnsigned(svpFundTxHashUnsigned); + + // act + bridgeStorageProvider.clearSvpValues(); + + // assert + assertNoSVPValues(); + } + + @Test + void clearSvpValues_whenFundTxSigned_shouldClearValue() { + // arrange + BtcTransaction svpFundTxSigned = new BtcTransaction(mainnetBtcParams); + bridgeStorageProvider.setSvpFundTxSigned(svpFundTxSigned); + + // act + bridgeStorageProvider.clearSvpValues(); + + // assert + assertNoSVPValues(); + } + + @Test + void clearSvpValues_whenSpendTxWFS_shouldClearSpendTxValues() { + // arrange + Keccak256 svpSpendTxCreationHash = RskTestUtils.createHash(1); + BtcTransaction svpSpendTx = new BtcTransaction(mainnetBtcParams); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendTxCreationHash, svpSpendTx); + bridgeStorageProvider.setSvpSpendTxWaitingForSignatures(svpSpendTxWFS); + + // act + bridgeStorageProvider.clearSvpValues(); + + // assert + assertNoSVPValues(); + } + + @Test + void clearSvpValues_whenSpendTxHashUnsigned_shouldClearValue() { + // arrange + Keccak256 svpSpendTxCreationHash = RskTestUtils.createHash(1); + BtcTransaction svpSpendTx = new BtcTransaction(mainnetBtcParams); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendTxCreationHash, svpSpendTx); + bridgeStorageProvider.setSvpSpendTxWaitingForSignatures(svpSpendTxWFS); + + // act + bridgeStorageProvider.clearSvpValues(); + + // assert + assertNoSVPValues(); + } + + private void assertNoSVPValues() { + assertFalse(bridgeStorageProvider.getSvpFundTxHashUnsigned().isPresent()); + assertFalse(bridgeStorageProvider.getSvpFundTxSigned().isPresent()); + assertFalse(bridgeStorageProvider.getSvpSpendTxWaitingForSignatures().isPresent()); + assertFalse(bridgeStorageProvider.getSvpSpendTxHashUnsigned().isPresent()); + } + } + @Test void getReleaseRequestQueue_before_rskip_146_activation() throws IOException { Repository repositoryMock = mock(Repository.class); diff --git a/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java b/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java index 0290676b0d..eb35646de8 100644 --- a/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java +++ b/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java @@ -16,6 +16,7 @@ import co.rsk.peg.federation.*; import co.rsk.peg.federation.constants.FederationConstants; import co.rsk.peg.feeperkb.FeePerKbSupport; +import co.rsk.peg.storage.InMemoryStorage; import co.rsk.peg.utils.BridgeEventLogger; import co.rsk.peg.utils.BridgeEventLoggerImpl; import co.rsk.test.builders.BridgeSupportBuilder; @@ -47,21 +48,24 @@ import static org.mockito.Mockito.when; public class BridgeSupportSvpTest { - private static final RskAddress bridgeContractAddress = PrecompiledContracts.BRIDGE_ADDR; - private final CallTransaction.Function releaseRequestedEvent = BridgeEvents.RELEASE_REQUESTED.getEvent(); - private final CallTransaction.Function pegoutTransactionCreatedEvent = BridgeEvents.PEGOUT_TRANSACTION_CREATED.getEvent(); - private final CallTransaction.Function addSignatureEvent = BridgeEvents.ADD_SIGNATURE.getEvent(); - private final CallTransaction.Function releaseBtcEvent = BridgeEvents.RELEASE_BTC.getEvent(); private static final ActivationConfig.ForBlock allActivations = ActivationConfigsForTest.all().forBlock(0); - + private static final RskAddress bridgeContractAddress = PrecompiledContracts.BRIDGE_ADDR; private static final BridgeConstants bridgeMainNetConstants = BridgeMainNetConstants.getInstance(); private static final NetworkParameters btcMainnetParams = bridgeMainNetConstants.getBtcParams(); private static final FederationConstants federationMainNetConstants = bridgeMainNetConstants.getFederationConstants(); private static final Coin spendableValueFromProposedFederation = bridgeMainNetConstants.getSpendableValueFromProposedFederation(); + private static final CallTransaction.Function releaseRequestedEvent = BridgeEvents.RELEASE_REQUESTED.getEvent(); + private static final CallTransaction.Function pegoutTransactionCreatedEvent = BridgeEvents.PEGOUT_TRANSACTION_CREATED.getEvent(); + private static final CallTransaction.Function addSignatureEvent = BridgeEvents.ADD_SIGNATURE.getEvent(); + private static final CallTransaction.Function releaseBtcEvent = BridgeEvents.RELEASE_BTC.getEvent(); + private static final CallTransaction.Function commitFederationFailedEvent = BridgeEvents.COMMIT_FEDERATION_FAILED.getEvent(); + private final BridgeSupportBuilder bridgeSupportBuilder = BridgeSupportBuilder.builder(); + private List logs; + private Block rskExecutionBlock; private Transaction rskTx; @@ -112,12 +116,129 @@ void setUp() { ); } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Tag("SVP failure tests") + class SVPFailureTests { + @BeforeEach + void setUp() { + logs = new ArrayList<>(); + BridgeEventLogger bridgeEventLogger = new BridgeEventLoggerImpl( + bridgeMainNetConstants, + allActivations, + logs + ); + + InMemoryStorage storageAccessor = new InMemoryStorage(); + FederationStorageProvider federationStorageProvider = new FederationStorageProviderImpl(storageAccessor); + federationStorageProvider.setProposedFederation(proposedFederation); + + federationSupport = new FederationSupportImpl( + bridgeMainNetConstants.getFederationConstants(), + federationStorageProvider, + rskExecutionBlock, + allActivations + ); + + bridgeSupport = bridgeSupportBuilder + .withBridgeConstants(bridgeMainNetConstants) + .withProvider(bridgeStorageProvider) + .withEventLogger(bridgeEventLogger) + .withActivations(allActivations) + .withFederationSupport(federationSupport) + .withFeePerKbSupport(feePerKbSupport) + .withExecutionBlock(rskExecutionBlock) + .build(); + } + + @Test + void processValidationFailure_whenSvpFundTxHashUnsigned_shouldLogValidationFailureAndClearValue() { + // arrange + Sha256Hash svpFundTxHashUnsigned = BitcoinTestUtils.createHash(1); + bridgeStorageProvider.setSvpFundTxHashUnsigned(svpFundTxHashUnsigned); + + // act + bridgeSupport.processSVPFailure(proposedFederation); + + // assert + assertLogCommitFederationFailed(); + assertNoProposedFederation(); + assertNoSVPValues(); + } + + @Test + void processValidationFailure_whenSvpFundTxSigned_shouldLogValidationFailureAndClearValue() { + // arrange + BtcTransaction svpFundTx = new BtcTransaction(btcMainnetParams); + bridgeStorageProvider.setSvpFundTxSigned(svpFundTx); + + // act + bridgeSupport.processSVPFailure(proposedFederation); + + // assert + assertLogCommitFederationFailed(); + assertNoProposedFederation(); + assertNoSVPValues(); + } + + @Test + void processValidationFailure_whenSvpSpendTxWFS_shouldLogValidationFailureAndClearSpendTxValues() { + // arrange + Keccak256 svpSpendTxCreationHash = RskTestUtils.createHash(1); + BtcTransaction svpSpendTx = new BtcTransaction(btcMainnetParams); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendTxCreationHash, svpSpendTx); + bridgeStorageProvider.setSvpSpendTxWaitingForSignatures(svpSpendTxWFS); + + // act + bridgeSupport.processSVPFailure(proposedFederation); + + // assert + assertLogCommitFederationFailed(); + assertNoProposedFederation(); + assertNoSVPValues(); + } + + @Test + void processValidationFailure_whenSvpSpendTxHashUnsigned_shouldLogValidationFailureAndClearValue() { + // arrange + Sha256Hash svpSpendTxHash = BitcoinTestUtils.createHash(2); + bridgeStorageProvider.setSvpSpendTxHashUnsigned(svpSpendTxHash); + + // act + bridgeSupport.processSVPFailure(proposedFederation); + + // assert + assertLogCommitFederationFailed(); + assertNoProposedFederation(); + assertNoSVPValues(); + } + + private void assertLogCommitFederationFailed() { + List encodedTopics = getEncodedTopics(commitFederationFailedEvent); + + byte[] proposedFederationRedeemScriptSerialized = proposedFederation.getRedeemScript().getProgram(); + byte[] encodedData = getEncodedData(commitFederationFailedEvent, proposedFederationRedeemScriptSerialized, rskExecutionBlock.getNumber()); + + assertEventWasEmittedWithExpectedTopics(encodedTopics); + assertEventWasEmittedWithExpectedData(encodedData); + } + + private void assertNoProposedFederation() { + assertFalse(federationSupport.getProposedFederation().isPresent()); + } + + private void assertNoSVPValues() { + assertFalse(bridgeStorageProvider.getSvpFundTxHashUnsigned().isPresent()); + assertFalse(bridgeStorageProvider.getSvpFundTxSigned().isPresent()); + assertFalse(bridgeStorageProvider.getSvpSpendTxWaitingForSignatures().isPresent()); + assertFalse(bridgeStorageProvider.getSvpSpendTxHashUnsigned().isPresent()); + } + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Tag("svp fund transaction creation and processing tests") class SvpFundTxCreationAndProcessingTests { - private List logs; - private BtcTransaction svpFundTransactionUnsigned; private Sha256Hash svpFundTransactionHashUnsigned; @@ -195,8 +316,8 @@ private void assertSvpReleaseWasSettled() throws IOException { svpFundTransactionUnsigned = getSvpFundTransactionFromPegoutsMap(pegoutsWaitingForConfirmations); assertPegoutTxSigHashWasSaved(svpFundTransactionUnsigned); - assertLogReleaseRequested(logs, rskTx.getHash(), svpFundTransactionHashUnsigned, spendableValueFromProposedFederation); - assertLogPegoutTransactionCreated(logs, svpFundTransactionUnsigned); + assertLogReleaseRequested(rskTx.getHash(), svpFundTransactionHashUnsigned, spendableValueFromProposedFederation); + assertLogPegoutTransactionCreated(svpFundTransactionUnsigned); } private BtcTransaction getSvpFundTransactionFromPegoutsMap(PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations) { @@ -490,7 +611,6 @@ private void assertSvpFundTransactionValuesWereUpdated() { @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Tag("svp spend transaction creation and processing tests") class SvpSpendTxCreationAndProcessingTests { - private List logs; private BtcTransaction svpFundTransaction; private Sha256Hash svpSpendTransactionHashUnsigned; private BtcTransaction svpSpendTransactionUnsigned; @@ -566,9 +686,9 @@ void processSvpSpendTransaction_createsExpectedTransactionAndSavesTheValuesAndLo assertSvpFundTxSignedWasRemovedFromStorage(); - assertLogPegoutTransactionCreated(logs, svpSpendTransactionUnsigned); + assertLogPegoutTransactionCreated(svpSpendTransactionUnsigned); Coin valueSentToActiveFed = Coin.valueOf(1762); - assertLogReleaseRequested(logs, rskTx.getHash(), svpSpendTransactionHashUnsigned, valueSentToActiveFed); + assertLogReleaseRequested(rskTx.getHash(), svpSpendTransactionHashUnsigned, valueSentToActiveFed); } private void assertSvpSpendTxHashUnsignedWasSavedInStorage() { @@ -649,7 +769,6 @@ class SvpSpendTxSigning { private BtcTransaction svpSpendTx; private List svpSpendTxSigHashes; - private List logs; private BridgeEventLogger bridgeEventLogger; @BeforeEach @@ -868,8 +987,8 @@ private void assertLogAddSignature(FederationMember federationMember, byte[] rsk BtcECKey federatorBtcPublicKey = federationMember.getBtcPublicKey(); byte[] encodedData = getEncodedData(addSignatureEvent, federatorBtcPublicKey.getPubKey()); - assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); - assertEventWasEmittedWithExpectedData(logs, encodedData); + assertEventWasEmittedWithExpectedTopics(encodedTopics); + assertEventWasEmittedWithExpectedData(encodedData); } private void assertLogReleaseBtc(Keccak256 rskTxHash, BtcTransaction btcTx) { @@ -879,8 +998,8 @@ private void assertLogReleaseBtc(Keccak256 rskTxHash, BtcTransaction btcTx) { byte[] btcTxSerialized = btcTx.bitcoinSerialize(); byte[] encodedData = getEncodedData(releaseBtcEvent, btcTxSerialized); - assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); - assertEventWasEmittedWithExpectedData(logs, encodedData); + assertEventWasEmittedWithExpectedTopics(encodedTopics); + assertEventWasEmittedWithExpectedData(encodedData); } private void assertFederatorSignedInputs(List inputs, List sigHashes, BtcECKey key) { @@ -996,18 +1115,18 @@ private void assertOutputWasSentToExpectedScriptWithExpectedAmount(List logs, Keccak256 releaseCreationTxHash, Sha256Hash pegoutTransactionHash, Coin requestedAmount) { + private void assertLogReleaseRequested(Keccak256 releaseCreationTxHash, Sha256Hash pegoutTransactionHash, Coin requestedAmount) { byte[] releaseCreationTxHashSerialized = releaseCreationTxHash.getBytes(); byte[] pegoutTransactionHashSerialized = pegoutTransactionHash.getBytes(); List encodedTopics = getEncodedTopics(releaseRequestedEvent, releaseCreationTxHashSerialized, pegoutTransactionHashSerialized); byte[] encodedData = getEncodedData(releaseRequestedEvent, requestedAmount.getValue()); - assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); - assertEventWasEmittedWithExpectedData(logs, encodedData); + assertEventWasEmittedWithExpectedTopics(encodedTopics); + assertEventWasEmittedWithExpectedData(encodedData); } - private void assertLogPegoutTransactionCreated(List logs, BtcTransaction pegoutTransaction) { + private void assertLogPegoutTransactionCreated(BtcTransaction pegoutTransaction) { Sha256Hash pegoutTransactionHash = pegoutTransaction.getHash(); byte[] pegoutTransactionHashSerialized = pegoutTransactionHash.getBytes(); List encodedTopics = getEncodedTopics(pegoutTransactionCreatedEvent, pegoutTransactionHashSerialized); @@ -1016,8 +1135,8 @@ private void assertLogPegoutTransactionCreated(List logs, BtcTransactio byte[] serializedOutpointValues = UtxoUtils.encodeOutpointValues(outpointValues); byte[] encodedData = getEncodedData(pegoutTransactionCreatedEvent, serializedOutpointValues); - assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); - assertEventWasEmittedWithExpectedData(logs, encodedData); + assertEventWasEmittedWithExpectedTopics(encodedTopics); + assertEventWasEmittedWithExpectedData(encodedData); } private List getEncodedTopics(CallTransaction.Function bridgeEvent, Object... args) { @@ -1029,14 +1148,14 @@ private byte[] getEncodedData(CallTransaction.Function bridgeEvent, Object... ar return bridgeEvent.encodeEventData(args); } - private void assertEventWasEmittedWithExpectedTopics(List logs, List expectedTopics) { + private void assertEventWasEmittedWithExpectedTopics(List expectedTopics) { Optional topicOpt = logs.stream() .filter(log -> log.getTopics().equals(expectedTopics)) .findFirst(); assertTrue(topicOpt.isPresent()); } - private void assertEventWasEmittedWithExpectedData(List logs, byte[] expectedData) { + private void assertEventWasEmittedWithExpectedData(byte[] expectedData) { Optional data = logs.stream() .filter(log -> Arrays.equals(log.getData(), expectedData)) .findFirst(); diff --git a/rskj-core/src/test/java/co/rsk/peg/federation/FederationSupportImplTest.java b/rskj-core/src/test/java/co/rsk/peg/federation/FederationSupportImplTest.java index 788108b368..242c7f9a28 100644 --- a/rskj-core/src/test/java/co/rsk/peg/federation/FederationSupportImplTest.java +++ b/rskj-core/src/test/java/co/rsk/peg/federation/FederationSupportImplTest.java @@ -2171,6 +2171,28 @@ void clearRetiredFederation_whenHavingOldFederation_removesOldFederation() { assertThat(oldFederation, is(nullValue())); } + @Test + @Tag("clear proposed federation") + void clearProposedFederation_removesProposedFederation() { + // arrange + ActivationConfig.ForBlock activations = ActivationConfigsForTest.all().forBlock(0); + + ErpFederation federation = P2shErpFederationBuilder.builder().build(); + storageProvider.setProposedFederation(federation); + + // first check the proposed federation was correctly saved + Optional proposedFederation = storageProvider.getProposedFederation(federationMainnetConstants, activations); + assertTrue(proposedFederation.isPresent()); + assertThat(proposedFederation.get(), is(federation)); + + // act + federationSupport.clearProposedFederation(); + + // assert + Optional currentProposedFederation = storageProvider.getProposedFederation(federationMainnetConstants, activations); + assertFalse(currentProposedFederation.isPresent()); + } + @Test @Tag("save") void save_callsStorageProviderSave() {