From 6c78fe6cee99366dff8d168f5bcf643c891bcea5 Mon Sep 17 00:00:00 2001 From: Antonio Pancorbo <48168255+apancorb@users.noreply.github.com> Date: Tue, 21 May 2024 12:56:30 +0000 Subject: [PATCH] feat(pegout): integrate pegout signed cache with btc release client refactor(pegout): improve naming for btc release client and pegout signed cache integration test feat(pegout): add btc release client test where a pegout is waited until it becomes invalid by pegouts signed cache feat(pegout): add java time clock to pegouts signed cache feat(refactor): use deterministic clock for pegout signed cache tests refactor(pegout): remove unused imports for pegouts signed cache test --- .../btcreleaseclient/BtcReleaseClient.java | 35 ++- .../cache/PegoutSignedCacheImpl.java | 10 +- .../BtcReleaseClientTest.java | 256 ++++++++++++++++++ .../cache/PegoutSignedCacheImplTest.java | 27 +- 4 files changed, 304 insertions(+), 24 deletions(-) diff --git a/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java b/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java index 89ed4a689..2c283def6 100644 --- a/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java +++ b/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java @@ -12,6 +12,8 @@ import co.rsk.crypto.Keccak256; import co.rsk.federate.FederatorSupport; import co.rsk.federate.adapter.ThinConverter; +import co.rsk.federate.btcreleaseclient.cache.PegoutSignedCache; +import co.rsk.federate.btcreleaseclient.cache.PegoutSignedCacheImpl; import co.rsk.federate.config.FedNodeSystemProperties; import co.rsk.federate.signing.ECDSASigner; import co.rsk.federate.signing.FederationCantSignException; @@ -35,6 +37,7 @@ import co.rsk.peg.federation.Federation; import co.rsk.peg.federation.ErpFederation; import co.rsk.peg.StateForFederator; +import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -84,11 +87,11 @@ public class BtcReleaseClient { private final Ethereum ethereum; private final FederatorSupport federatorSupport; - private final FedNodeSystemProperties systemProperties; private final Set observedFederations; private final NodeBlockProcessor nodeBlockProcessor; private final BridgeConstants bridgeConstants; private final boolean isPegoutEnabled; + private final PegoutSignedCache pegoutSignedCache; private ECDSASigner signer; private BtcReleaseEthereumListener blockListener; @@ -108,12 +111,13 @@ public BtcReleaseClient( ) { this.ethereum = ethereum; this.federatorSupport = federatorSupport; - this.systemProperties = systemProperties; this.observedFederations = new HashSet<>(); this.blockListener = new BtcReleaseEthereumListener(); - this.bridgeConstants = this.systemProperties.getNetworkConstants().getBridgeConstants(); - this.isPegoutEnabled = this.systemProperties.isPegoutEnabled(); + this.bridgeConstants = systemProperties.getNetworkConstants().getBridgeConstants(); + this.isPegoutEnabled = systemProperties.isPegoutEnabled(); this.nodeBlockProcessor = nodeBlockProcessor; + this.pegoutSignedCache = new PegoutSignedCacheImpl( + systemProperties.getPegoutSignedCacheTtl(), Clock.systemUTC()); } public void setup( @@ -143,7 +147,7 @@ public void setup( } peerGroup.start(); - blockListener = new BtcReleaseEthereumListener(); + this.blockListener = new BtcReleaseEthereumListener(); this.signerMessageBuilderFactory = signerMessageBuilderFactory; this.releaseCreationInformationGetter = pegoutCreationInformationGetter; this.releaseRequirementsEnforcer = releaseRequirementsEnforcer; @@ -283,10 +287,15 @@ protected Optional tryGetReleaseInformation( BtcTransaction pegoutBtcTx ) { try { + // Discard pegout tx if processed in a previous round of execution + logger.trace( + "[tryGetReleaseInformation] Checking if pegoutCreationTxHash {} has already been signed", + pegoutCreationRskTxHash); + validateTxIsNotCached(pegoutCreationRskTxHash); + // Discard pegout btc tx this fed already signed or cannot be signed by the observed federations logger.trace("[tryGetReleaseInformation] Validating if pegoutBtcTxHash {} can be signed by observed federations and " + "that it is not already signed by current fed", pegoutBtcTx.getHash()); - validateTxCanBeSigned(pegoutBtcTx); // IMPORTANT: As per the current behaviour of the bridge, no pegout should have inputs to be signed @@ -326,6 +335,15 @@ protected Optional tryGetReleaseInformation( return Optional.empty(); } + void validateTxIsNotCached(Keccak256 pegoutCreationRskTxHash) throws FederatorAlreadySignedException { + if (pegoutSignedCache.hasAlreadyBeenSigned(pegoutCreationRskTxHash)) { + String message = String.format( + "Rsk pegout creation tx hash %s was found in the pegouts signed cache", + pegoutCreationRskTxHash); + throw new FederatorAlreadySignedException(message); + } + } + protected void validateTxCanBeSigned(BtcTransaction pegoutBtcTx) throws FederatorAlreadySignedException, FederationCantSignException { try { BtcECKey federatorPublicKey = signer.getPublicKey(BTC_KEY_ID.getKeyId()).toBtcKey(); @@ -400,6 +418,11 @@ protected void signRelease(int signerVersion, ReleaseCreationInformation pegoutC logger.info("[signRelease] Signed pegout created in rsk transaction {}", pegoutCreationInformation.getPegoutConfirmationRskTxHash()); federatorSupport.addSignature(signatures, pegoutCreationInformation.getPegoutConfirmationRskTxHash().getBytes()); + + logger.trace("[signRelease] Put pegoutCreationRskTxHash {} in the pegouts signed cache", + pegoutCreationInformation.getPegoutCreationRskTxHash()); + pegoutSignedCache.putIfAbsent( + pegoutCreationInformation.getPegoutCreationRskTxHash()); } catch (SignerException e) { String message = String.format("Error signing pegout created in rsk transaction %s", pegoutCreationInformation.getPegoutCreationRskTxHash()); logger.error(message, e); diff --git a/src/main/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImpl.java b/src/main/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImpl.java index 28f0fcd58..2cdb92954 100644 --- a/src/main/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImpl.java +++ b/src/main/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImpl.java @@ -1,6 +1,7 @@ package co.rsk.federate.btcreleaseclient.cache; import co.rsk.crypto.Keccak256; +import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Map; @@ -20,10 +21,13 @@ public class PegoutSignedCacheImpl implements PegoutSignedCache { private final Map cache = new ConcurrentHashMap<>(); private final ScheduledExecutorService cleanupScheduler = Executors.newSingleThreadScheduledExecutor(); private final Duration ttl; + private final Clock clock; - public PegoutSignedCacheImpl(Duration ttl) { + public PegoutSignedCacheImpl(Duration ttl, Clock clock) { validateTtl(ttl); + this.ttl = ttl; + this.clock = clock; // Start a background thread for periodic cleanup cleanupScheduler.scheduleAtFixedRate( @@ -50,7 +54,7 @@ public void putIfAbsent(Keccak256 pegoutCreationRskTxHash) { } Optional.of(pegoutCreationRskTxHash) - .ifPresent(rskTxHash -> cache.putIfAbsent(rskTxHash, Instant.now())); + .ifPresent(rskTxHash -> cache.putIfAbsent(rskTxHash, clock.instant())); } void performCleanup() { @@ -64,7 +68,7 @@ void performCleanup() { private boolean isValidTimestamp(Instant timestampInCache) { return Optional.ofNullable(timestampInCache) - .map(timestamp -> Instant.now().toEpochMilli() - timestamp.toEpochMilli()) + .map(timestamp -> clock.instant().toEpochMilli() - timestamp.toEpochMilli()) .map(timeCachedInMillis -> timeCachedInMillis <= ttl.toMillis()) .orElse(false); } diff --git a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java index a7c041797..3395a4b58 100644 --- a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java +++ b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java @@ -2,6 +2,7 @@ import static co.rsk.federate.signing.PowPegNodeKeyId.BTC_KEY_ID; import static co.rsk.federate.signing.utils.TestUtils.createHash; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -35,6 +36,8 @@ import co.rsk.peg.constants.BridgeConstants; import co.rsk.crypto.Keccak256; import co.rsk.federate.FederatorSupport; +import co.rsk.federate.btcreleaseclient.cache.PegoutSignedCache; +import co.rsk.federate.btcreleaseclient.cache.PegoutSignedCacheImpl; import co.rsk.federate.config.FedNodeSystemProperties; import co.rsk.federate.mock.SimpleEthereumImpl; import co.rsk.federate.signing.ECDSASigner; @@ -59,8 +62,12 @@ import co.rsk.peg.federation.*; import co.rsk.peg.StateForFederator; +import java.lang.reflect.Field; import java.math.BigInteger; +import java.time.Clock; +import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -89,6 +96,9 @@ import org.spongycastle.util.encoders.Hex; class BtcReleaseClientTest { + + private final static Duration PEGOUT_SIGNED_CACHE_TTL = Duration.ofMinutes(30); + private NetworkParameters params; private BridgeConstants bridgeConstants; @@ -109,6 +119,8 @@ void if_start_not_called_rsk_blockchain_not_listened() { Ethereum ethereum = mock(Ethereum.class); FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); Mockito.doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); new BtcReleaseClient( ethereum, @@ -125,6 +137,8 @@ void when_start_called_rsk_blockchain_is_listened() { Ethereum ethereum = mock(Ethereum.class); FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); Mockito.doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, @@ -147,6 +161,8 @@ void if_stop_called_with_just_one_federation_rsk_blockchain_is_still_listened() Ethereum ethereum = mock(Ethereum.class); FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); Mockito.doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, @@ -172,6 +188,8 @@ void if_stop_called_with_federations_rsk_blockchain_is_not_listened() { Ethereum ethereum = mock(Ethereum.class); FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); Mockito.doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, @@ -218,6 +236,8 @@ void processReleases_ok() throws Exception { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); SignerMessageBuilder messageBuilder = new SignerMessageBuilderV1(releaseTx); SignerMessageBuilderFactory signerMessageBuilderFactory = mock(SignerMessageBuilderFactory.class); @@ -316,6 +336,8 @@ void having_two_pegouts_signs_only_one() throws Exception { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); doReturn(true).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( mock(ReceiptStore.class) @@ -380,6 +402,218 @@ void having_two_pegouts_signs_only_one() throws Exception { ); } + @Test + void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Exception { + Federation federation = TestUtils.createFederation(params, 9); + BtcTransaction pegout = TestUtils.createBtcTransaction(params, federation); + Keccak256 pegoutCreationRskTxHash = createHash(0); + SortedMap rskTxsWaitingForSignatures = new TreeMap<>(); + rskTxsWaitingForSignatures.put(pegoutCreationRskTxHash, pegout); + StateForFederator stateForFederator = new StateForFederator(rskTxsWaitingForSignatures); + + Ethereum ethereum = mock(Ethereum.class); + AtomicReference ethereumListener = new AtomicReference<>(); + doAnswer((InvocationOnMock invocation) -> { + ethereumListener.set((EthereumListener) invocation.getArguments()[0]); + return null; + }).when(ethereum).addListener(any(EthereumListener.class)); + + FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + + ECKey ecKey = new ECKey(); + BtcECKey fedKey = new BtcECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(fedKey.getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC_KEY_ID.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); + doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + BlockStore blockStore = mock(BlockStore.class); + ReceiptStore receiptStore = mock(ReceiptStore.class); + + Keccak256 blockHash = createHash(2); + Block block = mock(Block.class); + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(blockStore.getBlockByHash(blockHash.getBytes())).thenReturn(block); + when(txInfo.getReceipt()).thenReturn(txReceipt); + when(txInfo.getBlockHash()).thenReturn(blockHash.getBytes()); + when(receiptStore.getInMainChain(pegoutCreationRskTxHash.getBytes(), blockStore)).thenReturn(Optional.of(txInfo)); + + ReleaseCreationInformationGetter releaseCreationInformationGetter = + new ReleaseCreationInformationGetter( + receiptStore, blockStore + ); + + BtcReleaseClientStorageSynchronizer storageSynchronizer = + mock(BtcReleaseClientStorageSynchronizer.class); + when(storageSynchronizer.isSynced()).thenReturn(true); + + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( + ethereum, + federatorSupport, + fedNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + btcReleaseClient.setup( + signer, + mock(ActivationConfig.class), + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(federation); + + // At this point there is nothing in the pegouts signed cache, + // so it should not throw an exception + assertDoesNotThrow( + () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); + + // Start first round of execution + ethereumListener.get().onBestBlock(null, Collections.emptyList()); + + // After the first round of execution, we should throw an exception + // since we have signed the pegout and sent it to the bridge + assertThrows(FederatorAlreadySignedException.class, + () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); + + // Execute second round of execution + ethereumListener.get().onBestBlock(null, Collections.emptyList()); + + // Verify we only send the add_signature tx to the bridge once + // throughout both rounds of execution + verify(federatorSupport, times(1)).addSignature( + anyList(), + any(byte[].class) + ); + } + + @Test + void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTxAgain() throws Exception { + Federation federation = TestUtils.createFederation(params, 9); + BtcTransaction pegout = TestUtils.createBtcTransaction(params, federation); + Keccak256 pegoutCreationRskTxHash = createHash(0); + SortedMap rskTxsWaitingForSignatures = new TreeMap<>(); + rskTxsWaitingForSignatures.put(pegoutCreationRskTxHash, pegout); + StateForFederator stateForFederator = new StateForFederator(rskTxsWaitingForSignatures); + + Ethereum ethereum = mock(Ethereum.class); + AtomicReference ethereumListener = new AtomicReference<>(); + doAnswer((InvocationOnMock invocation) -> { + ethereumListener.set((EthereumListener) invocation.getArguments()[0]); + return null; + }).when(ethereum).addListener(any(EthereumListener.class)); + + FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + + ECKey ecKey = new ECKey(); + BtcECKey fedKey = new BtcECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(fedKey.getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC_KEY_ID.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); + doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + BlockStore blockStore = mock(BlockStore.class); + ReceiptStore receiptStore = mock(ReceiptStore.class); + + Keccak256 blockHash = createHash(2); + Block block = mock(Block.class); + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(blockStore.getBlockByHash(blockHash.getBytes())).thenReturn(block); + when(txInfo.getReceipt()).thenReturn(txReceipt); + when(txInfo.getBlockHash()).thenReturn(blockHash.getBytes()); + when(receiptStore.getInMainChain(pegoutCreationRskTxHash.getBytes(), blockStore)).thenReturn(Optional.of(txInfo)); + + ReleaseCreationInformationGetter releaseCreationInformationGetter = + new ReleaseCreationInformationGetter( + receiptStore, blockStore + ); + + BtcReleaseClientStorageSynchronizer storageSynchronizer = + mock(BtcReleaseClientStorageSynchronizer.class); + when(storageSynchronizer.isSynced()).thenReturn(true); + + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( + ethereum, + federatorSupport, + fedNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + Clock baseClock = Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PegoutSignedCache pegoutSignedCache = new PegoutSignedCacheImpl(PEGOUT_SIGNED_CACHE_TTL, baseClock); + Field field = btcReleaseClient.getClass().getDeclaredField("pegoutSignedCache"); + field.setAccessible(true); + field.set(btcReleaseClient, pegoutSignedCache); + + btcReleaseClient.setup( + signer, + mock(ActivationConfig.class), + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(federation); + + // Start first round of execution + ethereumListener.get().onBestBlock(null, Collections.emptyList()); + + // Ensure the pegout tx becomes invalid by advancing the clock 1 hour + field = pegoutSignedCache.getClass().getDeclaredField("clock"); + field.setAccessible(true); + field.set(pegoutSignedCache, Clock.offset(baseClock, Duration.ofHours(1))); + + // At this point the pegout tx is invalid in the pegouts signed cache, + // so it should not throw an exception + assertDoesNotThrow( + () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); + + // Execute second round of execution + ethereumListener.get().onBestBlock(null, Collections.emptyList()); + + // Verify we send the add_signature tx to the bridge twice + // throughout both rounds of execution + verify(federatorSupport, times(2)).addSignature( + anyList(), + any(byte[].class) + ); + } + @Test void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException { // Arrange @@ -398,6 +632,8 @@ void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); doReturn(true).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); NodeBlockProcessor nodeBlockProcessor = mock(NodeBlockProcessor.class); when(nodeBlockProcessor.hasBetterBlockToSync()).thenReturn(true); @@ -444,6 +680,8 @@ void onBestBlock_return_when_pegout_is_disabled() throws BtcReleaseClientExcepti FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); doReturn(false).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); NodeBlockProcessor nodeBlockProcessor = mock(NodeBlockProcessor.class); when(nodeBlockProcessor.hasBetterBlockToSync()).thenReturn(false); @@ -490,6 +728,8 @@ void onBlock_return_when_node_is_syncing() { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); doReturn(true).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); NodeBlockProcessor nodeBlockProcessor = mock(NodeBlockProcessor.class); when(nodeBlockProcessor.hasBetterBlockToSync()).thenReturn(true); @@ -532,6 +772,8 @@ void onBlock_return_when_pegout_is_disabled() { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); doReturn(Constants.regtest()).when(fedNodeSystemProperties).getNetworkConstants(); doReturn(false).when(fedNodeSystemProperties).isPegoutEnabled(); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); NodeBlockProcessor nodeBlockProcessor = mock(NodeBlockProcessor.class); when(nodeBlockProcessor.hasBetterBlockToSync()).thenReturn(false); @@ -640,6 +882,8 @@ void validateTxCanBeSigned_federatorAlreadySigned() throws Exception { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); ECPublicKey signerPublicKey = new ECPublicKey(federator1PrivKey.getPubKey()); ECDSASigner signer = mock(ECDSASigner.class); @@ -678,6 +922,8 @@ void validateTxCanBeSigned_federationCantSign() throws Exception { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcECKey fed1Key = federation.getBtcPublicKeys().get(0); ECPublicKey signerPublicKey = new ECPublicKey(fed1Key.getPubKey()); @@ -739,6 +985,8 @@ void removeSignaturesFromTransaction() { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient client = new BtcReleaseClient( mock(Ethereum.class), @@ -815,6 +1063,8 @@ private void test_validateTxCanBeSigned( ) throws Exception { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); ECDSASigner signer = mock(ECDSASigner.class); doReturn(signerPublicKey).when(signer).getPublicKey(any(KeyId.class)); @@ -846,6 +1096,8 @@ private void test_extractStandardRedeemScript( { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient client = new BtcReleaseClient( mock(Ethereum.class), @@ -917,6 +1169,8 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); when(fedNodeSystemProperties.isPegoutEnabled()).thenReturn(true); + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); NodeBlockProcessor nodeBlockProcessor = mock(NodeBlockProcessor.class); when(nodeBlockProcessor.hasBetterBlockToSync()).thenReturn(false); @@ -1022,6 +1276,8 @@ private BtcReleaseClient createBtcClient() { FedNodeSystemProperties fedNodeSystemProperties = mock(FedNodeSystemProperties.class); when(fedNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); when(fedNodeSystemProperties.isPegoutEnabled()).thenReturn(true); // Enabled by default + when(fedNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); return new BtcReleaseClient( mock(Ethereum.class), diff --git a/src/test/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImplTest.java b/src/test/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImplTest.java index 8d2945ea6..76b2a9b3a 100644 --- a/src/test/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImplTest.java +++ b/src/test/java/co/rsk/federate/btcreleaseclient/cache/PegoutSignedCacheImplTest.java @@ -2,16 +2,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import co.rsk.crypto.Keccak256; import co.rsk.federate.signing.utils.TestUtils; import java.lang.reflect.Field; +import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,7 +27,8 @@ class PegoutSignedCacheImplTest { private static final Keccak256 PEGOUT_CREATION_RSK_HASH = TestUtils.createHash(1); private final Map cache = new ConcurrentHashMap<>(); - private final PegoutSignedCache pegoutSignedCache = new PegoutSignedCacheImpl(DEFAULT_TTL); + private final Clock clock = Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + private final PegoutSignedCache pegoutSignedCache = new PegoutSignedCacheImpl(DEFAULT_TTL, clock); @BeforeEach void setUp() throws Exception { @@ -45,7 +46,7 @@ void constructor_shouldThrowIllegalArgumentException_whenTtlIsInvalid(Long ttl) "Invalid pegouts signed cache TTL value in minutes supplied: %s", ttl); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new PegoutSignedCacheImpl(invalidTtl)); + () -> new PegoutSignedCacheImpl(invalidTtl, clock)); assertEquals(expectedErrorMessage, exception.getMessage()); } @@ -67,7 +68,7 @@ void hasAlreadyBeenSigned_shouldReturnFalse_whenPegoutCreationRskTxHashIsNotCach @Test void hasAlreadyBeenSigned_shouldReturnFalse_whenCacheContainsInvalidTimestamp() { - Instant currentTimestamp = Instant.now(); + Instant currentTimestamp = clock.instant(); Instant invalidTimestamp = currentTimestamp.minus(60, ChronoUnit.MINUTES); cache.put(PEGOUT_CREATION_RSK_HASH, invalidTimestamp); @@ -78,7 +79,7 @@ void hasAlreadyBeenSigned_shouldReturnFalse_whenCacheContainsInvalidTimestamp() @Test void hasAlreadyBeenSigned_shouldReturnTrue_whenCacheContainsValidTimestamp() { - Instant currentTimestamp = Instant.now(); + Instant currentTimestamp = clock.instant(); Instant validTimestamp = currentTimestamp.minus(10, ChronoUnit.MINUTES); cache.put(PEGOUT_CREATION_RSK_HASH, validTimestamp); @@ -91,9 +92,9 @@ void hasAlreadyBeenSigned_shouldReturnTrue_whenCacheContainsValidTimestamp() { void putIfAbsent_shouldThrowIllegalArgumentException_whenPegoutCreationRskTxHashIsNull() { Keccak256 pegoutCreationRskTxHash = null; - assertEquals(0, cache.size()); assertThrows(IllegalArgumentException.class, () -> pegoutSignedCache.putIfAbsent(pegoutCreationRskTxHash)); + assertEquals(0, cache.size()); } @Test @@ -108,37 +109,33 @@ void putIfAbsent_shouldPutInCache_whenPegoutCreationRskTxHashIsNotNull() { void putIfAbsent_shouldPutInCacheBoth_whenPegoutCreationRskTxHashAreNotSame() { // first insert pegoutSignedCache.putIfAbsent(PEGOUT_CREATION_RSK_HASH); - Instant pegoutCreationRskTxHashTimestamp = cache.get(PEGOUT_CREATION_RSK_HASH); // second insert Keccak256 otherPegoutCreationRskTxHash = TestUtils.createHash(2); pegoutSignedCache.putIfAbsent(otherPegoutCreationRskTxHash); - Instant otherPegoutCreationRskTxHashTimestamp = cache.get(otherPegoutCreationRskTxHash); - assertNotSame(pegoutCreationRskTxHashTimestamp, otherPegoutCreationRskTxHashTimestamp); + assertEquals(2, cache.size()); } @Test void putIfAbsent_shouldPutInCacheOnce_whenPegoutCreationRskTxHashIsTheSame() { // first insert pegoutSignedCache.putIfAbsent(PEGOUT_CREATION_RSK_HASH); - Instant pegoutCreationRskTxHashTimestamp1 = cache.get(PEGOUT_CREATION_RSK_HASH); // second insert pegoutSignedCache.putIfAbsent(PEGOUT_CREATION_RSK_HASH); - Instant pegoutCreationRskTxHashTimestamp2 = cache.get(PEGOUT_CREATION_RSK_HASH); - assertSame(pegoutCreationRskTxHashTimestamp1, pegoutCreationRskTxHashTimestamp2); + assertEquals(1, cache.size()); } @Test void performCleanup_shouldRemoveOnlyInvalidPegouts_whenPerformCleanupIsTriggered() throws Exception { // setup cache - PegoutSignedCacheImpl pegoutSignedCacheImpl = new PegoutSignedCacheImpl(DEFAULT_TTL); + PegoutSignedCacheImpl pegoutSignedCacheImpl = new PegoutSignedCacheImpl(DEFAULT_TTL, clock); Field field = pegoutSignedCacheImpl.getClass().getDeclaredField("cache"); field.setAccessible(true); field.set(pegoutSignedCacheImpl, cache); // put a valid and invalid timestamp in the cache - Instant currentTimestamp = Instant.now(); + Instant currentTimestamp = clock.instant(); Instant validTimestamp = currentTimestamp.minus(10, ChronoUnit.MINUTES); Instant notValidTimestamp = currentTimestamp.minus(60, ChronoUnit.MINUTES); Keccak256 otherPegoutCreationRskHash = TestUtils.createHash(2);