diff --git a/src/main/java/co/rsk/federate/BtcToRskClient.java b/src/main/java/co/rsk/federate/BtcToRskClient.java index 598d4f105..5ca0b42b3 100644 --- a/src/main/java/co/rsk/federate/BtcToRskClient.java +++ b/src/main/java/co/rsk/federate/BtcToRskClient.java @@ -111,14 +111,16 @@ public synchronized void setup( public void start(Federation federation) { logger.info("[start] Starting for Federation {}", federation.getAddress()); this.federation = federation; + FederationMember federator = federatorSupport.getFederationMember(); boolean isMember = federation.isMember(federator); - if (!isMember) { - logger.info("[start] member {} is no part of the federation {} ", + String message = String.format( + "Member %s is no part of the federation %s", federator.getBtcPublicKey(), federation.getAddress()); - return; + logger.error("[start] {}", message); + throw new IllegalStateException(message); } logger.info("[start] {} is member of the federation {}", @@ -126,9 +128,9 @@ public void start(Federation federation) { logger.info("[start] Watching federation {} since I belong to it", federation.getAddress()); bitcoinWrapper.addFederationListener(federation, this); + Optional federatorIndex = federation.getBtcPublicKeyIndex( - federatorSupport.getFederationMember().getBtcPublicKey() - ); + federatorSupport.getFederationMember().getBtcPublicKey()); if (!federatorIndex.isPresent()) { String message = String.format( "Federator %s is a member of the federation %s but could not find the btcPublicKeyIndex", @@ -138,22 +140,21 @@ public void start(Federation federation) { logger.error("[start] {}", message); throw new IllegalStateException(message); } - TurnScheduler scheduler = new TurnScheduler( - bridgeConstants.getUpdateBridgeExecutionPeriod(), - federation.getSize() - ); - long now = Clock.systemUTC().instant().toEpochMilli(); if (isUpdateBridgeTimerEnabled) { - updateBridgeTimer = Executors.newSingleThreadScheduledExecutor(); + long now = Clock.systemUTC().instant().toEpochMilli(); + TurnScheduler scheduler = new TurnScheduler( + bridgeConstants.getUpdateBridgeExecutionPeriod(), + federation.getSize()); + + this.updateBridgeTimer = Executors.newSingleThreadScheduledExecutor(); + updateBridgeTimer.scheduleAtFixedRate( this::updateBridge, scheduler.getDelay(now, federatorIndex.get()), scheduler.getInterval(), - TimeUnit.MILLISECONDS - ); - } - else { + TimeUnit.MILLISECONDS); + } else { logger.info("[start] updateBridgeTimer is disabled"); } } @@ -161,11 +162,11 @@ public void start(Federation federation) { public void stop() { logger.info("Stopping"); - federation = null; + this.federation = null; if (updateBridgeTimer != null) { updateBridgeTimer.shutdown(); - updateBridgeTimer = null; + this.updateBridgeTimer = null; } } @@ -177,13 +178,15 @@ public void updateBridge() { if (federation == null) { logger.warn("[updateBridge] updateBridge skipped because no Federation is associated to this BtcToRskClient"); } + if (nodeBlockProcessor.hasBetterBlockToSync()) { logger.warn("[updateBridge] updateBridge skipped because the node is syncing blocks"); return; } + logger.debug("[updateBridge] Updating bridge"); - if(shouldUpdateBridgeBtcBlockchain) { + if (shouldUpdateBridgeBtcBlockchain) { // Call receiveHeaders try { int numberOfBlocksSent = updateBridgeBtcBlockchain(); @@ -194,7 +197,7 @@ public void updateBridge() { } } - if(shouldUpdateBridgeBtcCoinbaseTransactions) { + if (shouldUpdateBridgeBtcCoinbaseTransactions) { // Call registerBtcCoinbaseTransaction try { logger.debug("[updateBridge] Updating transactions and sending update"); @@ -205,7 +208,7 @@ public void updateBridge() { } } - if(shouldUpdateBridgeBtcTransactions) { + if (shouldUpdateBridgeBtcTransactions) { // Call registerBtcTransaction try { logger.debug("[updateBridge] Updating transactions and sending update"); @@ -216,7 +219,7 @@ public void updateBridge() { } } - if(shouldUpdateCollections) { + if (shouldUpdateCollections) { // Call updateCollections try { logger.debug("[updateBridge] Sending updateCollections"); diff --git a/src/main/java/co/rsk/federate/FedNodeContext.java b/src/main/java/co/rsk/federate/FedNodeContext.java index 88a76fe83..cb7abecfd 100644 --- a/src/main/java/co/rsk/federate/FedNodeContext.java +++ b/src/main/java/co/rsk/federate/FedNodeContext.java @@ -29,6 +29,7 @@ import co.rsk.federate.signing.hsm.advanceblockchain.HSMBookKeepingClientProvider; import co.rsk.federate.signing.hsm.client.HSMClientProtocolFactory; import co.rsk.federate.solidity.DummySolidityCompiler; +import co.rsk.federate.watcher.FederationWatcher; import java.util.concurrent.TimeUnit; import org.ethereum.rpc.Web3; import org.ethereum.solidity.compiler.SolidityCompiler; diff --git a/src/main/java/co/rsk/federate/FedNodeRunner.java b/src/main/java/co/rsk/federate/FedNodeRunner.java index 705922c83..67518a0e5 100644 --- a/src/main/java/co/rsk/federate/FedNodeRunner.java +++ b/src/main/java/co/rsk/federate/FedNodeRunner.java @@ -52,7 +52,9 @@ import co.rsk.federate.signing.hsm.message.SignerMessageBuilderFactory; import co.rsk.federate.signing.hsm.requirements.AncestorBlockUpdater; import co.rsk.federate.signing.hsm.requirements.ReleaseRequirementsEnforcer; -import co.rsk.peg.federation.Federation; +import co.rsk.federate.watcher.FederationWatcher; +import co.rsk.federate.watcher.FederationWatcherListener; +import co.rsk.federate.watcher.FederationWatcherListenerImpl; import co.rsk.peg.federation.FederationMember; import co.rsk.peg.btcLockSender.BtcLockSenderProvider; import co.rsk.peg.pegininstructions.PeginInstructionsProvider; @@ -65,7 +67,6 @@ import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; /** @@ -342,27 +343,14 @@ private void startFederate() throws Exception { config.getBtcReleaseClientInitializationMaxDepth() ) ); - federationWatcher.setup(federationProvider); - - federationWatcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - String oldFederationAddress = oldFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - String newFederationAddress = newFederation.getAddress().toString(); - logger.debug(String.format("[onActiveFederationChange] Active federation change: from %s to %s", oldFederationAddress, newFederationAddress)); - triggerClientChange(btcToRskClientActive, Optional.of(newFederation)); - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - String oldFederationAddress = oldFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - String newFederationAddress = newFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - logger.debug(String.format("[onRetiringFederationChange] Retiring federation change: from %s to %s", oldFederationAddress, newFederationAddress)); - triggerClientChange(btcToRskClientRetiring, newFederation); - } - }); - // Trigger the first events - federationWatcher.updateState(); + + FederationWatcherListener federationWatcherListener = new FederationWatcherListenerImpl( + btcToRskClientActive, + btcToRskClientRetiring, + btcReleaseClient, + bitcoinWrapper); + + federationWatcher.start(federationProvider, federationWatcherListener); } } @@ -384,22 +372,6 @@ private void shutdown() { System.exit(-1); } - // TODO: This this method (and this whole class) - private void triggerClientChange(BtcToRskClient client, Optional federation) { - client.stop(); - federation.ifPresent(btcReleaseClient::stop); - // Only start if this federator is part of the new federation - if (federation.isPresent() && federation.get().isMember(this.member)) { - String federationAddress = federation.get().getAddress().toString(); - logger.debug("[triggerClientChange] Starting lock and release clients since I belong to federation {}", federationAddress); - logger.info("[triggerClientChange] Joined to {} federation", federationAddress); - client.start(federation.get()); - btcReleaseClient.start(federation.get()); - } else { - logger.warn("[triggerClientChange] This federator node is not part of the new federation. Check your configuration for signers BTC, RSK and MST keys"); - } - } - @Override public void stop() { logger.info("[stop] Shutting down Federation node"); diff --git a/src/main/java/co/rsk/federate/FederationProvider.java b/src/main/java/co/rsk/federate/FederationProvider.java index 244bf2749..64b1e9584 100644 --- a/src/main/java/co/rsk/federate/FederationProvider.java +++ b/src/main/java/co/rsk/federate/FederationProvider.java @@ -15,34 +15,59 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - package co.rsk.federate; import co.rsk.bitcoinj.core.Address; import co.rsk.peg.federation.Federation; - -import java.util.List; import java.util.Optional; /** - * Implementors of this interface must be able to provide - * federation instances. - * - * @author Ariel Mendelzon + * Provides access to various federation instances, including the active, retiring, and proposed federations. + * Implementations of this interface must define how to retrieve the federations and their respective addresses. + * These methods allow clients to manage and monitor federations in different lifecycle stages within the bridge. */ public interface FederationProvider { - // The currently "active" federation + + /** + * Retrieves the currently active federation. + * + * @return the active {@link Federation} instance + */ Federation getActiveFederation(); - // The currently "active" federation's address + + /** + * Retrieves the address of the currently active federation. + * + * @return the {@link Address} of the active federation + */ Address getActiveFederationAddress(); - // The currently "retiring" federation + /** + * Retrieves the currently retiring federation, if one exists. This federation is in transition + * and will soon be replaced by a new active federation. + * + * @return an {@link Optional} containing the retiring {@link Federation}, or {@link Optional#empty()} if none exists + */ Optional getRetiringFederation(); - // The currently "retiring" federation's address + + /** + * Retrieves the address of the currently retiring federation, if one exists. + * + * @return an {@link Optional} containing the {@link Address} of the retiring federation, or {@link Optional#empty()} if none exists + */ Optional
getRetiringFederationAddress(); - // The federations that are "live", that is, are still - // operational. This should be the active federation - // plus the retiring federation, if one exists - List getLiveFederations(); + /** + * Retrieves the currently proposed federation, if one exists. This federation is awaiting validation. + * + * @return an {@link Optional} containing the proposed {@link Federation}, or {@link Optional#empty()} if none exists + */ + Optional getProposedFederation(); + + /** + * Retrieves the address of the currently proposed federation, if one exists. + * + * @return an {@link Optional} containing the {@link Address} of the proposed federation, or {@link Optional#empty()} if none exists + */ + Optional
getProposedFederationAddress(); } diff --git a/src/main/java/co/rsk/federate/FederationProviderFromFederatorSupport.java b/src/main/java/co/rsk/federate/FederationProviderFromFederatorSupport.java index 56e4a8ed7..9dbe78598 100644 --- a/src/main/java/co/rsk/federate/FederationProviderFromFederatorSupport.java +++ b/src/main/java/co/rsk/federate/FederationProviderFromFederatorSupport.java @@ -15,38 +15,45 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - package co.rsk.federate; +import static co.rsk.peg.federation.FederationChangeResponseCode.FEDERATION_NON_EXISTENT; +import static co.rsk.peg.federation.FederationMember.KeyType; +import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP123; +import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP419; + import co.rsk.bitcoinj.core.Address; import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.bitcoinj.core.NetworkParameters; import co.rsk.peg.federation.*; import co.rsk.peg.federation.constants.FederationConstants; -import org.ethereum.config.blockchain.upgrades.ActivationConfig; -import org.ethereum.crypto.ECKey; - import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP123; +import java.util.stream.IntStream; +import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.crypto.ECKey; /** - * Provides a federation using a FederatorSupport instance, which in turn - * gathers the federation from the bridge contract of the ethereum - * network it is attached to. + * A provider that supplies the current active, retiring, and proposed federations by + * using a {@link FederatorSupport} instance, which interacts with the Bridge contract. * - * @author Ariel Mendelzon + *

The {@code FederationProviderFromFederatorSupport} enables access to: + *

    + *
  • Active Federation + *
  • Retiring Federation + *
  • Proposed Federation + *
*/ public class FederationProviderFromFederatorSupport implements FederationProvider { + private final FederatorSupport federatorSupport; private final FederationConstants federationConstants; public FederationProviderFromFederatorSupport( FederatorSupport federatorSupport, FederationConstants federationConstants) { - this.federatorSupport = federatorSupport; this.federationConstants = federationConstants; } @@ -92,13 +99,12 @@ public Address getActiveFederationAddress() { @Override public Optional getRetiringFederation() { Integer federationSize = federatorSupport.getRetiringFederationSize(); - Optional
optionalRetiringFederationAddress = getRetiringFederationAddress(); - if (federationSize == -1 || !optionalRetiringFederationAddress.isPresent()) { + if (federationSize == FEDERATION_NON_EXISTENT.getCode()) { return Optional.empty(); } - Address retiringFederationAddress = optionalRetiringFederationAddress.get(); + Address retiringFederationAddress = getRetiringFederationAddress().orElseThrow(IllegalStateException::new); boolean useTypedPublicKeyGetter = federatorSupport.getConfigForBestBlock().isActive(RSKIP123); List members = new ArrayList<>(); for (int i = 0; i < federationSize; i++) { @@ -137,14 +143,52 @@ public Optional
getRetiringFederationAddress() { } @Override - public List getLiveFederations() { - List result = new ArrayList<>(); - result.add(getActiveFederation()); + public Optional getProposedFederation() { + if (!federatorSupport.getConfigForBestBlock().isActive(RSKIP419)) { + return Optional.empty(); + } - Optional retiringFederation = getRetiringFederation(); - retiringFederation.ifPresent(result::add); + Integer federationSize = federatorSupport.getProposedFederationSize() + .orElse(FEDERATION_NON_EXISTENT.getCode()); + if (federationSize == FEDERATION_NON_EXISTENT.getCode()) { + return Optional.empty(); + } + + List federationMembers = IntStream.range(0, federationSize) + .mapToObj(i -> new FederationMember( + federatorSupport.getProposedFederatorPublicKeyOfType(i, KeyType.BTC) + .map(ECKey::getPubKey) + .map(BtcECKey::fromPublicOnly) + .orElseThrow(IllegalStateException::new), + federatorSupport.getProposedFederatorPublicKeyOfType(i, KeyType.RSK) + .orElseThrow(IllegalStateException::new), + federatorSupport.getProposedFederatorPublicKeyOfType(i, KeyType.MST) + .orElseThrow(IllegalStateException::new) + )) + .toList(); + + FederationArgs federationArgs = new FederationArgs( + federationMembers, + federatorSupport.getProposedFederationCreationTime() + .orElseThrow(IllegalStateException::new), + federatorSupport.getProposedFederationCreationBlockNumber() + .orElseThrow(IllegalStateException::new), + federatorSupport.getBtcParams() + ); + + Federation federation = FederationFactory.buildP2shErpFederation( + federationArgs, + federationConstants.getErpFedPubKeysList(), + federationConstants.getErpFedActivationDelay()); + + return Optional.of(federation); + } - return result; + @Override + public Optional
getProposedFederationAddress() { + return Optional.of(federatorSupport) + .filter(fedSupport -> fedSupport.getConfigForBestBlock().isActive(RSKIP419)) + .flatMap(FederatorSupport::getProposedFederationAddress); } private Federation getExpectedFederation(Federation initialFederation, Address expectedFederationAddress) { diff --git a/src/main/java/co/rsk/federate/FederationWatcher.java b/src/main/java/co/rsk/federate/FederationWatcher.java deleted file mode 100644 index 7d47379fa..000000000 --- a/src/main/java/co/rsk/federate/FederationWatcher.java +++ /dev/null @@ -1,104 +0,0 @@ -package co.rsk.federate; - -import co.rsk.bitcoinj.core.Address; -import co.rsk.peg.federation.Federation; -import org.ethereum.core.TransactionReceipt; -import org.ethereum.facade.Ethereum; -import org.ethereum.listener.EthereumListenerAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Watches the RSK blockchain for federation changes, and informs - * when a federation changes. - * @author Ariel Mendelzon - */ -public class FederationWatcher { - private static final Logger logger = LoggerFactory.getLogger("FederationWatcher"); - - private final Ethereum rsk; - - private FederationProvider federationProvider; - - private List listeners = new ArrayList<>(); - - // Previous recorded federations - private Optional activeFederation = Optional.empty(); - private Optional retiringFederation = Optional.empty(); - - public FederationWatcher(Ethereum rsk) { - this.rsk = rsk; - } - - public interface Listener { - void onActiveFederationChange(Optional oldFederation, Federation newFederation); - void onRetiringFederationChange(Optional oldFederation, Optional newFederation); - } - - public void setup(FederationProvider federationProvider) throws Exception { - this.federationProvider = federationProvider; - rsk.addListener(new FederationWatcherRskListener()); - } - - public void addListener(Listener listener) { - listeners.add(listener); - } - - private class FederationWatcherRskListener extends EthereumListenerAdapter { - @Override - public void onBestBlock(org.ethereum.core.Block block, List receipts) { - // Updating state only when the best block changes still "works", - // since we're interested in finding out only when the active or retiring federation(s) changed. - // If there was a side chain in which any of these changed in, say, block 4500, but - // that side chain became main chain in block 7800, it would still be ok to - // start monitoring the new federation(s) on that block. - // The main reasons for this is that RSK nodes would have never reported the active or - // retiring federation(s) being different *before* the best chain change. Therefore, - // there should be no new Bitcoin transactions directed to these new addresses - // until this change effectively becomes a part of RSK's "reality". - // The case in which we go back and forth to and from a sidechain in which the - // federation effectively changed is still to be explored deeply, but the same reasoning - // should apply since going back and forth would trigger two federation changes. - // A client trying to send bitcoins to the new federation without waiting - // a good number of confirmations would be, essentially, "playing with fire". - logger.info("New best block, updating state"); - updateState(); - } - } - - public void updateState() { - // Active federation changed? - // We compare addresses first so as not to do innecessary calls to the bridge - Address currentlyActiveFederationAddress = federationProvider.getActiveFederationAddress(); - if (!currentlyActiveFederationAddress.equals(activeFederation.map(Federation::getAddress).orElse(null))) { - // Gather the active federation and inform listeners of the change - Federation currentlyActiveFederation = federationProvider.getActiveFederation(); - String oldActive = activeFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - String newActive = currentlyActiveFederation.getAddress().toString(); - logger.info(String.format("Active federation changed from %s to %s", oldActive, newActive)); - for (Listener l : listeners) { - l.onActiveFederationChange(activeFederation, currentlyActiveFederation); - } - activeFederation = Optional.of(currentlyActiveFederation); - } - - // Retiring federation changed? - // We compare addresses first so as not to do innecessary calls to the bridge - Optional
currentlyRetiringFederationAddress = federationProvider.getRetiringFederationAddress(); - if (!retiringFederation.map(Federation::getAddress).equals(currentlyRetiringFederationAddress)) { - // Gather the retiring federation and inform listeners of the change - Optional currentlyRetiringFederation = federationProvider.getRetiringFederation(); - String oldRetiring = retiringFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - String newRetiring = currentlyRetiringFederation.map(f -> f.getAddress().toString()).orElse("NONE"); - logger.info(String.format("Retiring federation changed from %s to %s", oldRetiring, newRetiring)); - for (Listener l : listeners) { - l.onRetiringFederationChange(retiringFederation, currentlyRetiringFederation); - } - retiringFederation = currentlyRetiringFederation; - } - } -} diff --git a/src/main/java/co/rsk/federate/FederatorSupport.java b/src/main/java/co/rsk/federate/FederatorSupport.java index 47b746c6f..a271797f1 100644 --- a/src/main/java/co/rsk/federate/FederatorSupport.java +++ b/src/main/java/co/rsk/federate/FederatorSupport.java @@ -11,19 +11,21 @@ import co.rsk.peg.Bridge; import co.rsk.peg.federation.FederationMember; import co.rsk.peg.StateForFederator; +import co.rsk.peg.StateForProposedFederator; import org.bitcoinj.core.PartialMerkleTree; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.Sha256Hash; import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.ethereum.core.Blockchain; import org.ethereum.crypto.ECKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.math.BigInteger; import java.net.UnknownHostException; import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -32,17 +34,14 @@ */ public class FederatorSupport { - private static final Logger LOGGER = LoggerFactory.getLogger(FederatorSupport.class); + private static final Logger logger = LoggerFactory.getLogger(FederatorSupport.class); private final Blockchain blockchain; private final PowpegNodeSystemProperties config; - private final NetworkParameters parameters; - private final BridgeTransactionSender bridgeTransactionSender; private ECDSASigner signer; - private FederationMember federationMember; private RskAddress federatorAddress; @@ -52,9 +51,7 @@ public FederatorSupport( BridgeTransactionSender bridgeTransactionSender) { this.blockchain = blockchain; this.config = config; - this.parameters = config.getNetworkConstants().getBridgeConstants().getBtcParams(); - this.bridgeTransactionSender = bridgeTransactionSender; } @@ -99,7 +96,7 @@ public Long getRskBestChainHeight() { } public void sendReceiveHeaders(org.bitcoinj.core.Block[] headers) { - LOGGER.debug("About to send to the bridge headers from {} to {}", headers[0].getHash(), headers[headers.length - 1].getHash()); + logger.debug("About to send to the bridge headers from {} to {}", headers[0].getHash(), headers[headers.length - 1].getHash()); Object[] objectArray = new Object[headers.length]; @@ -119,7 +116,7 @@ public Long getBtcTxHashProcessedHeight(Sha256Hash btcTxHash) { } public void sendRegisterBtcTransaction(org.bitcoinj.core.Transaction tx, int blockHeight, PartialMerkleTree pmt) { - LOGGER.debug("About to send to the bridge btc tx hash {}. Block height {}", tx.getWTxId(), blockHeight); + logger.debug("About to send to the bridge btc tx hash {}. Block height {}", tx.getWTxId(), blockHeight); byte[] txSerialized = tx.bitcoinSerialize(); byte[] pmtSerialized = pmt.bitcoinSerialize(); @@ -127,7 +124,7 @@ public void sendRegisterBtcTransaction(org.bitcoinj.core.Transaction tx, int blo } public void sendRegisterCoinbaseTransaction(CoinbaseInformation coinbaseInformation) { - LOGGER.debug("About to send to the bridge btc coinbase tx hash {}. Block hash {}", coinbaseInformation.getCoinbaseTransaction().getTxId(), coinbaseInformation.getBlockHash()); + logger.debug("About to send to the bridge btc coinbase tx hash {}. Block hash {}", coinbaseInformation.getCoinbaseTransaction().getTxId(), coinbaseInformation.getBlockHash()); byte[] txSerialized = coinbaseInformation.getSerializedCoinbaseTransactionWithoutWitness(); byte[] pmtSerialized = coinbaseInformation.getPmt().bitcoinSerialize(); @@ -151,6 +148,13 @@ public StateForFederator getStateForFederator() { return new StateForFederator(result, this.parameters); } + public Optional getStateForProposedFederator() { + byte[] result = bridgeTransactionSender.callTx( + federatorAddress, Bridge.GET_STATE_FOR_SVP_CLIENT); + + return Optional.ofNullable(result) + .map(rlpData -> new StateForProposedFederator(rlpData, parameters)); + } public void addSignature(List signatures, byte[] rskTxHash) { byte[] federatorPublicKeyBytes = federationMember.getBtcPublicKey().getPubKey(); @@ -187,7 +191,11 @@ public ECKey getFederatorPublicKeyOfType(int index, FederationMember.KeyType key public Instant getFederationCreationTime() { BigInteger federationCreationTime = this.bridgeTransactionSender.callTx(federatorAddress, Bridge.GET_FEDERATION_CREATION_TIME); - return Instant.ofEpochMilli(federationCreationTime.longValue()); + + if (!getConfigForBestBlock().isActive(ConsensusRule.RSKIP419)) { + return Instant.ofEpochMilli(federationCreationTime.longValue()); + } + return Instant.ofEpochSecond(federationCreationTime.longValue()); } public long getFederationCreationBlockNumber() { @@ -198,7 +206,7 @@ public long getFederationCreationBlockNumber() { public Optional
getRetiringFederationAddress() { String addressString = this.bridgeTransactionSender.callTx(federatorAddress, Bridge.GET_RETIRING_FEDERATION_ADDRESS); - if (addressString.equals("")) { + if (addressString.isEmpty()) { return Optional.empty(); } @@ -245,12 +253,61 @@ public ECKey getRetiringFederatorPublicKeyOfType(int index, FederationMember.Key } public Instant getRetiringFederationCreationTime() { - BigInteger creationTime = this.bridgeTransactionSender.callTx(federatorAddress, Bridge.GET_FEDERATION_CREATION_TIME); + BigInteger creationTime = this.bridgeTransactionSender.callTx(federatorAddress, Bridge.GET_RETIRING_FEDERATION_CREATION_TIME); if (creationTime == null) { return null; } - return Instant.ofEpochMilli(creationTime.longValue()); + if (!getConfigForBestBlock().isActive(ConsensusRule.RSKIP419)) { + return Instant.ofEpochMilli(creationTime.longValue()); + } + return Instant.ofEpochSecond(creationTime.longValue()); + } + + public Optional
getProposedFederationAddress() { + String proposedFederationAddress = bridgeTransactionSender.callTx( + federatorAddress, Bridge.GET_PROPOSED_FEDERATION_ADDRESS); + + return Optional.ofNullable(proposedFederationAddress) + .filter(addr -> !addr.isEmpty()) + .map(addr -> Address.fromBase58(getBtcParams(), addr)); + } + + public Optional getProposedFederationSize() { + BigInteger size = bridgeTransactionSender.callTx( + federatorAddress, Bridge.GET_PROPOSED_FEDERATION_SIZE); + + return Optional.ofNullable(size) + .map(BigInteger::intValue); + } + + public Optional getProposedFederatorPublicKeyOfType(int index, FederationMember.KeyType keyType) { + Objects.requireNonNull(keyType); + + byte[] publicKeyBytes = bridgeTransactionSender.callTx( + federatorAddress, + Bridge.GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE, + new Object[]{ index, keyType.getValue() }); + + return Optional.ofNullable(publicKeyBytes) + .map(ECKey::fromPublicOnly); + } + + public Optional getProposedFederationCreationTime() { + BigInteger creationTime = bridgeTransactionSender.callTx( + federatorAddress, Bridge.GET_PROPOSED_FEDERATION_CREATION_TIME); + + return Optional.ofNullable(creationTime) + .map(BigInteger::longValue) + .map(Instant::ofEpochSecond); + } + + public Optional getProposedFederationCreationBlockNumber() { + BigInteger creationBlockNumber = bridgeTransactionSender.callTx( + federatorAddress, Bridge.GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER); + + return Optional.ofNullable(creationBlockNumber) + .map(BigInteger::longValue); } public int getBtcBlockchainBestChainHeight() { diff --git a/src/main/java/co/rsk/federate/bitcoin/BitcoinWrapperImpl.java b/src/main/java/co/rsk/federate/bitcoin/BitcoinWrapperImpl.java index c8665f919..af353de8b 100644 --- a/src/main/java/co/rsk/federate/bitcoin/BitcoinWrapperImpl.java +++ b/src/main/java/co/rsk/federate/bitcoin/BitcoinWrapperImpl.java @@ -43,6 +43,7 @@ * @author Oscar Guindzberg */ public class BitcoinWrapperImpl implements BitcoinWrapper { + private class FederationListener { private Federation federation; private TransactionListener listener; @@ -73,6 +74,9 @@ public boolean equals(Object o) { } } + private static final int MAX_SIZE_MAP_STORED_BLOCKS = 10_000; + private static final Logger LOGGER = LoggerFactory.getLogger(BitcoinWrapperImpl.class); + private Context btcContext; private BridgeConstants bridgeConstants; private boolean running = false; @@ -86,9 +90,6 @@ public boolean equals(Object o) { private final FederatorSupport federatorSupport; private final Kit kit; - public static final int MAX_SIZE_MAP_STORED_BLOCKS = 10_000; - private static final Logger LOGGER = LoggerFactory.getLogger(BitcoinWrapperImpl.class); - public BitcoinWrapperImpl( Context btcContext, BridgeConstants bridgeConstants, @@ -316,42 +317,48 @@ public void removeNewBestBlockListener(NewBestBlockListener newBestBlockListener } protected void coinsReceivedOrSent(Transaction tx) { - if (watchedFederations.size() > 0) { - LOGGER.debug("[coinsReceivedOrSent] Received filtered transaction {}", tx.getWTxId().toString()); - Context.propagate(btcContext); - // Wrap tx in a co.rsk.bitcoinj.core.BtcTransaction - BtcTransaction btcTx = ThinConverter.toThinInstance(bridgeConstants.getBtcParams(), tx); - co.rsk.bitcoinj.core.Context btcContextThin = ThinConverter.toThinInstance(btcContext); - for (FederationListener watched : watchedFederations) { - Federation watchedFederation = watched.getFederation(); - TransactionListener listener = watched.getListener(); - Wallet watchedFederationWallet = new BridgeBtcWallet(btcContextThin, Collections.singletonList(watchedFederation)); - if (PegUtilsLegacy.isValidPegInTx(btcTx, watchedFederation, watchedFederationWallet, bridgeConstants, federatorSupport.getConfigForBestBlock())) { - - PeginInformation peginInformation = new PeginInformation( - btcLockSenderProvider, - peginInstructionsProvider, - federatorSupport.getConfigForBestBlock() - ); - try { - peginInformation.parse(btcTx); - } catch (PeginInstructionsException e) { - // If tx sender could be retrieved then let the Bridge process the tx and refund the sender - if (peginInformation.getSenderBtcAddress() != null) { - LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is not a valid lock tx, funds will be refunded to sender", tx.getWTxId()); - } else { - LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is not a valid lock tx and won't be processed!", tx.getWTxId()); - continue; - } - } + if (watchedFederations.isEmpty()) { + return; + } - LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is a lock", tx.getWTxId()); - listener.onTransaction(tx); - } - if (PegUtilsLegacy.isPegOutTx(btcTx, Collections.singletonList(watchedFederation), federatorSupport.getConfigForBestBlock())) { - LOGGER.debug("[coinsReceivedOrSent] [btctx with hash {} and witness hash {}] is a pegout", tx.getTxId(), tx.getWTxId()); - listener.onTransaction(tx); + LOGGER.debug("[coinsReceivedOrSent] Received filtered transaction {}", tx.getWTxId().toString()); + Context.propagate(btcContext); + + // Wrap tx in a co.rsk.bitcoinj.core.BtcTransaction + BtcTransaction btcTx = ThinConverter.toThinInstance(bridgeConstants.getBtcParams(), tx); + co.rsk.bitcoinj.core.Context btcContextThin = ThinConverter.toThinInstance(btcContext); + + for (FederationListener watched : watchedFederations) { + Federation watchedFederation = watched.getFederation(); + TransactionListener listener = watched.getListener(); + Wallet watchedFederationWallet = new BridgeBtcWallet(btcContextThin, Collections.singletonList(watchedFederation)); + + if (PegUtilsLegacy.isValidPegInTx(btcTx, watchedFederation, watchedFederationWallet, bridgeConstants, federatorSupport.getConfigForBestBlock())) { + PeginInformation peginInformation = new PeginInformation( + btcLockSenderProvider, + peginInstructionsProvider, + federatorSupport.getConfigForBestBlock() + ); + + try { + peginInformation.parse(btcTx); + } catch (PeginInstructionsException e) { + // If tx sender could be retrieved then let the Bridge process the tx and refund the sender + if (peginInformation.getSenderBtcAddress() != null) { + LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is not a valid lock tx, funds will be refunded to sender", tx.getWTxId()); + } else { + LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is not a valid lock tx and won't be processed!", tx.getWTxId()); + continue; + } } + + LOGGER.debug("[coinsReceivedOrSent] [btctx:{}] is a lock", tx.getWTxId()); + listener.onTransaction(tx); + } + + if (PegUtilsLegacy.isPegOutTx(btcTx, Collections.singletonList(watchedFederation), federatorSupport.getConfigForBestBlock())) { + LOGGER.debug("[coinsReceivedOrSent] [btctx with hash {} and witness hash {}] is a pegout", tx.getTxId(), tx.getWTxId()); + listener.onTransaction(tx); } } } diff --git a/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java b/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java index 154cb8b10..614bc449e 100644 --- a/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java +++ b/src/main/java/co/rsk/federate/btcreleaseclient/BtcReleaseClient.java @@ -1,5 +1,7 @@ package co.rsk.federate.btcreleaseclient; +import static co.rsk.federate.signing.PowPegNodeKeyId.BTC; + import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.bitcoinj.core.BtcTransaction; import co.rsk.bitcoinj.core.TransactionInput; @@ -36,8 +38,10 @@ import co.rsk.peg.BridgeEvents; import co.rsk.peg.BridgeUtils; import co.rsk.peg.federation.Federation; +import co.rsk.peg.federation.FederationMember; import co.rsk.peg.federation.ErpFederation; import co.rsk.peg.StateForFederator; +import co.rsk.peg.StateForProposedFederator; import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; @@ -59,8 +63,12 @@ import org.bitcoinj.script.ScriptPattern; import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.config.blockchain.upgrades.ConsensusRule; +import org.ethereum.core.Block; import org.ethereum.core.TransactionReceipt; import org.ethereum.crypto.ECKey; +import org.ethereum.db.BlockStore; +import org.ethereum.db.ReceiptStore; +import org.ethereum.db.TransactionInfo; import org.ethereum.facade.Ethereum; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.util.RLP; @@ -71,21 +79,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static co.rsk.federate.signing.PowPegNodeKeyId.BTC; - /** - * Manages signing and broadcasting pegouts - * @author Oscar Guindzberg + * Responsible for managing the signing and broadcasting of pegout transactions + * to the Bitcoin network in a federated bridge environment. The BtcReleaseClient + * coordinates the execution of pegout operations, ensuring transactions are + * correctly signed and propagated. + * + *

Key responsibilities include:

+ *
    + *
  • Assembling transaction data and managing signing processes
  • + *
  • Validating transaction information before broadcast
  • + *
  • Ensuring successful pegout transaction broadcast to the Bitcoin network
  • + *
*/ public class BtcReleaseClient { + private static final Logger logger = LoggerFactory.getLogger(BtcReleaseClient.class); private static final PanicProcessor panicProcessor = new PanicProcessor(); private static final List SINGLE_RELEASE_BTC_TOPIC_RLP = Collections.singletonList(Bridge.RELEASE_BTC_TOPIC); private static final DataWord SINGLE_RELEASE_BTC_TOPIC_SOLIDITY = DataWord.valueOf(BridgeEvents.RELEASE_BTC.getEvent().encodeSignatureLong()); - private ActivationConfig activationConfig; - private PeerGroup peerGroup; - private final Ethereum ethereum; private final FederatorSupport federatorSupport; private final Set observedFederations; @@ -94,13 +107,13 @@ public class BtcReleaseClient { private final boolean isPegoutEnabled; private final PegoutSignedCache pegoutSignedCache; + private ActivationConfig activationConfig; + private PeerGroup peerGroup; private ECDSASigner signer; private BtcReleaseEthereumListener blockListener; private SignerMessageBuilderFactory signerMessageBuilderFactory; - private ReleaseCreationInformationGetter releaseCreationInformationGetter; private ReleaseRequirementsEnforcer releaseRequirementsEnforcer; - private BtcReleaseClientStorageAccessor storageAccessor; private BtcReleaseClientStorageSynchronizer storageSynchronizer; @@ -134,7 +147,8 @@ public void setup( this.activationConfig = activationConfig; logger.debug("[setup] Signer: {}", signer.getClass()); - org.bitcoinj.core.Context btcContext = new org.bitcoinj.core.Context(ThinConverter.toOriginalInstance(bridgeConstants.getBtcParamsString())); + org.bitcoinj.core.Context btcContext = new org.bitcoinj.core.Context( + ThinConverter.toOriginalInstance(bridgeConstants.getBtcParamsString())); peerGroup = new PeerGroup(btcContext); try { if (!federatorSupport.getBitcoinPeerAddresses().isEmpty()) { @@ -143,7 +157,7 @@ public void setup( } peerGroup.setMaxConnections(federatorSupport.getBitcoinPeerAddresses().size()); } - } catch(Exception e) { + } catch (Exception e) { throw new BtcReleaseClientException("Error configuring peerSupport", e); } peerGroup.start(); @@ -160,32 +174,46 @@ public void setup( } public void start(Federation federation) { + FederationMember federationMember = federatorSupport.getFederationMember(); + if (!federation.isMember(federationMember)) { + String message = String.format( + "Member %s is no part of the federation %s", + federationMember.getBtcPublicKey(), + federation.getAddress()); + logger.error("[start] {}", message); + throw new IllegalStateException(message); + } + if (!observedFederations.contains(federation)) { observedFederations.add(federation); - logger.debug("[start] observing Federation {}", federation.getAddress()); + logger.info("[start] Observing federation {}", federation.getAddress()); } + if (observedFederations.size() == 1) { // If there is just one observed Federation, it means the btcReleaseClient wasn't started - logger.debug("[start] Starting"); - ethereum.addListener(this.blockListener); + logger.info("[start] Starting block listener"); + ethereum.addListener(blockListener); } } public void stop(Federation federation) { if (observedFederations.contains(federation)) { observedFederations.remove(federation); - logger.debug("[stop] not observing Federation {}", federation.getAddress()); + logger.info("[stop] Stopping observing federation {}", federation.getAddress()); } + if (observedFederations.isEmpty()) { // If there are no more observed Federations, the btcReleaseClient should stop - logger.debug("[stop] Stopping"); - ethereum.removeListener(this.blockListener); + logger.info("[stop] Stopping block listener"); + ethereum.removeListener(blockListener); } } @PreDestroy public void tearDown() { - org.bitcoinj.core.Context.propagate(new org.bitcoinj.core.Context(ThinConverter.toOriginalInstance(bridgeConstants.getBtcParamsString()))); + org.bitcoinj.core.Context.propagate( + new org.bitcoinj.core.Context( + ThinConverter.toOriginalInstance(bridgeConstants.getBtcParamsString()))); peerGroup.stop(); peerGroup = null; } @@ -193,27 +221,37 @@ public void tearDown() { private class BtcReleaseEthereumListener extends EthereumListenerAdapter { @Override public void onBestBlock(org.ethereum.core.Block block, List receipts) { + if (!isPegoutEnabled) { + logger.warn("[onBestBlock] Processing of RSK transactions waiting for signatures is disabled"); + return; + } + boolean hasBetterBlockToSync = nodeBlockProcessor.hasBetterBlockToSync(); boolean isStorageSynced = storageSynchronizer.isSynced(); if (hasBetterBlockToSync || !isStorageSynced) { logger.trace( - "[onBestBlock] Node is not ready to process pegouts. hasBetterBlockToSync: {} isStorageSynced: {}", + "[onBestBlock] Node is not ready to process pegouts. hasBetterBlockToSync: {} - isStorageSynced: {}", hasBetterBlockToSync, isStorageSynced ); return; } + storageSynchronizer.processBlock(block, receipts); + + // Sign svp spend tx waiting for signatures, if it exists, + // before attempting to sign any pegouts. + if (activationConfig.isActive(ConsensusRule.RSKIP419, block.getNumber())) { + federatorSupport.getStateForProposedFederator() + .map(StateForProposedFederator::getSvpSpendTxWaitingForSignatures) + .filter(svpSpendTxWaitingForSignatures -> isSVPSpendTxReadyToSign(block.getNumber(), svpSpendTxWaitingForSignatures)) + .ifPresent(svpSpendTxReadyToBeSigned -> processReleases(Set.of(svpSpendTxReadyToBeSigned))); + } + // Processing transactions waiting for signatures on best block only still "works", // since it all lies within RSK's blockchain and normal rules apply. I.e., this // process works on a block-by-block basis. StateForFederator stateForFederator = federatorSupport.getStateForFederator(); - storageSynchronizer.processBlock(block, receipts); - - // Delegate processing to our own method - logger.trace("[onBestBlock] Got {} pegouts", stateForFederator.getRskTxsWaitingForSignatures().entrySet().size()); - if (isPegoutEnabled) { - processReleases(stateForFederator.getRskTxsWaitingForSignatures().entrySet()); - } + processReleases(stateForFederator.getRskTxsWaitingForSignatures().entrySet()); } @Override @@ -221,6 +259,7 @@ public void onBlock(org.ethereum.core.Block block, List rece if (!isPegoutEnabled || nodeBlockProcessor.hasBetterBlockToSync()) { return; } + /* Pegout events must be processed on an every-single-block basis, since otherwise we could be missing pegouts potentially mined on what originally were side-chains and then turned into best-chains.*/ @@ -239,21 +278,62 @@ public void onBlock(org.ethereum.core.Block block, List rece pegoutTxs.forEach(BtcReleaseClient.this::onBtcRelease); } + /** + * Determines if the svp spend transaction hash is ready to be signed based on its block confirmations. + * + *

+ * This method retrieves the block associated with the given transaction hash and calculates + * the difference in block numbers between the current block and the block containing the transaction. + * If the difference meets or exceeds the required confirmation threshold defined in the bridge constants, + * the transaction is considered ready for signing. + *

+ * + * @param currentBlockNumber the current block number in the blockchain + * @param svpTxHash the Keccak256 hash of the svp spend transaction waiting to be signed + * @return {@code true} if the transaction has the required number of confirmations and is ready to be signed; + * {@code false} otherwise + */ + private boolean isSVPSpendTxReadyToSign(long currentBlockNumber, Map.Entry svpSpendTx) { + try { + int version = signer.getVersionForKeyId(BTC.getKeyId()); + ReleaseCreationInformation releaseCreationInformation = releaseCreationInformationGetter.getTxInfoToSign( + version, svpSpendTx.getKey(), svpSpendTx.getValue()); + + boolean isReadyToSign = Optional.ofNullable(releaseCreationInformation) + .map(ReleaseCreationInformation::getPegoutCreationBlock) + .map(Block::getNumber) + .map(blockNumberWithSvpSpendTx -> currentBlockNumber - blockNumberWithSvpSpendTx) + .filter(confirmationDifference -> confirmationDifference >= bridgeConstants.getRsk2BtcMinimumAcceptableConfirmations()) + .isPresent(); + + logger.info("[isSvpSpendTxReadyToSign] SVP spend tx readiness check for signing: tx hash [{}], Current block [{}], Ready to sign? [{}]", + svpSpendTx.getKey(), + currentBlockNumber, + isReadyToSign ? "YES" : "NO"); + + return isReadyToSign; + } catch (Exception e) { + logger.error("[isSvpSpendTxReadyToSign] Error ocurred while checking if SVP spend tx is ready to be signed", e); + + return false; + } + } + private BtcTransaction convertToBtcTxFromRLPData(byte[] dataFromBtcReleaseTopic) { - RLPList dataElements = (RLPList)RLP.decode2(dataFromBtcReleaseTopic).get(0); + RLPList dataElements = (RLPList) RLP.decode2(dataFromBtcReleaseTopic).get(0); return new BtcTransaction(bridgeConstants.getBtcParams(), dataElements.get(1).getRLPData()); } private BtcTransaction convertToBtcTxFromSolidityData(byte[] dataFromBtcReleaseTopic) { return new BtcTransaction(bridgeConstants.getBtcParams(), - (byte[])BridgeEvents.RELEASE_BTC.getEvent().decodeEventData(dataFromBtcReleaseTopic)[0]); + (byte[]) BridgeEvents.RELEASE_BTC.getEvent().decodeEventData(dataFromBtcReleaseTopic)[0]); } } protected void processReleases(Set> pegouts) { try { - logger.debug("[processReleases] Starting process with {} pegouts", pegouts.size()); + logger.info("[processReleases] Starting signing process with {} pegouts", pegouts.size()); int version = signer.getVersionForKeyId(BTC.getKeyId()); // Get pegout information and store it in a new list List pegoutsReadyToSign = new ArrayList<>(); diff --git a/src/main/java/co/rsk/federate/config/PowpegNodeConfigParameter.java b/src/main/java/co/rsk/federate/config/PowpegNodeConfigParameter.java index d57e7a18c..317d59615 100644 --- a/src/main/java/co/rsk/federate/config/PowpegNodeConfigParameter.java +++ b/src/main/java/co/rsk/federate/config/PowpegNodeConfigParameter.java @@ -4,13 +4,15 @@ import java.util.function.Function; public enum PowpegNodeConfigParameter { - FEDERATOR_ENABLED("federator.enabled", Boolean.toString(true)), - PEGOUT_ENABLED("federator.pegout.enabled", Boolean.toString(true)), - UPDATE_BRIDGE_TIMER_ENABLED("federator.updateBridgeTimerEnabled", Boolean.toString(true)), - UPDATE_BRIDGE_BTC_BLOCKCHAIN("federator.updateBridgeBtcBlockchain", Boolean.toString(true)), - UPDATE_BRIDGE_BTC_COINBASE_TRANSACTIONS("federator.updateBridgeBtcCoinbaseTransactions", Boolean.toString(true)), - UPDATE_BRIDGE_BTC_TRANSACTIONS("federator.updateBridgeBtcTransactions", Boolean.toString(true)), - UPDATE_COLLECTIONS("federator.updateCollections", Boolean.toString(true)), + FEDERATOR_ENABLED("federator.enabled", Boolean.TRUE.toString()), + // when enabled the federator will be able to attempt signing + // rsk transactions waiting for signatures reported by the Bridge + PEGOUT_ENABLED("federator.pegout.enabled", Boolean.TRUE.toString()), + UPDATE_BRIDGE_TIMER_ENABLED("federator.updateBridgeTimerEnabled", Boolean.TRUE.toString()), + UPDATE_BRIDGE_BTC_BLOCKCHAIN("federator.updateBridgeBtcBlockchain", Boolean.TRUE.toString()), + UPDATE_BRIDGE_BTC_COINBASE_TRANSACTIONS("federator.updateBridgeBtcCoinbaseTransactions", Boolean.TRUE.toString()), + UPDATE_BRIDGE_BTC_TRANSACTIONS("federator.updateBridgeBtcTransactions", Boolean.TRUE.toString()), + UPDATE_COLLECTIONS("federator.updateCollections", Boolean.TRUE.toString()), GAS_PRICE("federator.gasPrice", "0"), GAS_PRICE_PROVIDER("federator.gasPriceProvider", ""), AMOUNT_HEADERS("federator.amountOfHeadersToSend", "25"), diff --git a/src/main/java/co/rsk/federate/watcher/FederationWatcher.java b/src/main/java/co/rsk/federate/watcher/FederationWatcher.java new file mode 100644 index 000000000..99880ec75 --- /dev/null +++ b/src/main/java/co/rsk/federate/watcher/FederationWatcher.java @@ -0,0 +1,150 @@ +package co.rsk.federate.watcher; + +import co.rsk.bitcoinj.core.Address; +import co.rsk.federate.FederationProvider; +import co.rsk.peg.federation.Federation; +import org.ethereum.core.TransactionReceipt; +import org.ethereum.facade.Ethereum; +import org.ethereum.listener.EthereumListenerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Watches the RSK blockchain for changes to the active and retiring federations. + * This class listens for new blocks in the RSK blockchain and checks if the active or + * retiring federations have changed, notifying listeners when such changes occur. + */ +public class FederationWatcher { + + private static final Logger logger = LoggerFactory.getLogger(FederationWatcher.class); + + private final Ethereum rsk; + + private FederationProvider federationProvider; + private FederationWatcherListener federationWatcherListener; + + private Federation activeFederation; + private Federation retiringFederation; + private Federation proposedFederation; + + /** + * Constructs a new {@code FederationWatcher} with the specified RSK client. + * + * @param rsk the Ethereum client used to listen for new blocks on the RSK blockchain + */ + public FederationWatcher(Ethereum rsk) { + this.rsk = rsk; + } + + /** + * Starts the {@code FederationWatcher} by setting the {@code FederationProvider} + * and {@code FederationWatcherListener}, and begins listening for new blocks on + * the RSK blockchain. + * + * @param federationProvider the provider used to obtain federation information (active and retiring federations) + * @param federationWatcherListener the listener that will be notified when the federations change + */ + public void start(FederationProvider federationProvider, FederationWatcherListener federationWatcherListener) { + this.federationProvider = federationProvider; + this.federationWatcherListener = federationWatcherListener; + + rsk.addListener(new EthereumListenerAdapter() { + @Override + public void onBestBlock(org.ethereum.core.Block block, List receipts) { + // Updating state only when the best block changes still "works", + // since we're interested in finding out only when the active or retiring federation(s) changed. + // + // If there was a side chain in which any of these changed in, say, block 4500, but + // that side chain became main chain in block 7800, it would still be ok to + // start monitoring the new federation(s) on that block. + // + // The main reasons for this is that RSK nodes would have never reported the active or + // retiring federation(s) being different *before* the best chain change. Therefore, + // there should be no new Bitcoin transactions directed to these new addresses + // until this change effectively becomes a part of RSK's "reality". + // + // The case in which we go back and forth to and from a sidechain in which the + // federation effectively changed is still to be explored deeply, but the same reasoning + // should apply since going back and forth would trigger two federation changes. + // + // A client trying to send bitcoins to the new federation without waiting + // a good number of confirmations would be, essentially, "playing with fire". + logger.info("[onBestBlock] New best block, updating state"); + updateState(); + } + }); + } + + /** + * Updates the current state of the federations by checking if the active or + * retiring federations have changed. If a federation change is detected, it notifies + * the {@code FederationWatcherListener}. + */ + private void updateState() { + updateProposedFederation(); + updateActiveFederation(); + updateRetiringFederation(); + } + + private void updateProposedFederation() { + Optional
currentlyProposedFederationAddress = federationProvider.getProposedFederationAddress(); + Optional
oldProposedFederationAddress = Optional.ofNullable(proposedFederation) + .map(Federation::getAddress); + + boolean hasProposedFederationChanged = !currentlyProposedFederationAddress.equals(oldProposedFederationAddress); + + if (hasProposedFederationChanged) { + logger.info("[updateProposedFederation] Proposed federation changed from {} to {}", + oldProposedFederationAddress.orElse(null), + currentlyProposedFederationAddress.orElse(null)); + + Federation currentlyProposedFederation = federationProvider.getProposedFederation().orElse(null); + + federationWatcherListener.onProposedFederationChange(currentlyProposedFederation); + this.proposedFederation = currentlyProposedFederation; + } + } + + private void updateActiveFederation() { + Address currentlyActiveFederationAddress = Objects.requireNonNull( + federationProvider.getActiveFederationAddress(), "The current active federation should always exist"); + Address oldActiveFederationAddress = Optional.ofNullable(activeFederation) + .map(Federation::getAddress) + .orElse(null); + + boolean hasActiveFederationChanged = !currentlyActiveFederationAddress.equals(oldActiveFederationAddress); + + if (hasActiveFederationChanged) { + logger.info("[updateActiveFederation] Active federation changed from {} to {}", + oldActiveFederationAddress, + currentlyActiveFederationAddress); + + Federation currentlyActiveFederation = federationProvider.getActiveFederation(); + + federationWatcherListener.onActiveFederationChange(currentlyActiveFederation); + this.activeFederation = currentlyActiveFederation; + } + } + + private void updateRetiringFederation() { + Optional
currentlyRetiringFederationAddress = federationProvider.getRetiringFederationAddress(); + Optional
oldRetiringFederationAddress = Optional.ofNullable(retiringFederation) + .map(Federation::getAddress); + + boolean hasRetiringFederationChanged = !currentlyRetiringFederationAddress.equals(oldRetiringFederationAddress); + + if (hasRetiringFederationChanged) { + logger.info("[updateRetiringFederation] Retiring federation changed from {} to {}", + oldRetiringFederationAddress.orElse(null), + currentlyRetiringFederationAddress.orElse(null)); + + Federation currentlyRetiringFederation = federationProvider.getRetiringFederation().orElse(null); + + federationWatcherListener.onRetiringFederationChange(currentlyRetiringFederation); + this.retiringFederation = currentlyRetiringFederation; + } + } +} diff --git a/src/main/java/co/rsk/federate/watcher/FederationWatcherListener.java b/src/main/java/co/rsk/federate/watcher/FederationWatcherListener.java new file mode 100644 index 000000000..5d40eba79 --- /dev/null +++ b/src/main/java/co/rsk/federate/watcher/FederationWatcherListener.java @@ -0,0 +1,34 @@ +package co.rsk.federate.watcher; + +import co.rsk.peg.federation.Federation; + +/** + * A listener interface for receiving notifications about changes in federations. + * Implementers of this interface will be notified when the active or retiring federation changes. + */ +public interface FederationWatcherListener { + + /** + * Invoked when the active federation changes. + * + * @param newActiveFederation the new active federation after the change. + * This will never be {@code null}; the active federation is always present. + */ + void onActiveFederationChange(Federation newActiveFederation); + + /** + * Invoked when the retiring federation changes. + * + * @param newRetiringFederation the new retiring federation after the change. + * This can be {@code null}; the retiring federation is not always present. + */ + void onRetiringFederationChange(Federation newRetiringFederation); + + /** + * Invoked when the proposed federation changes. + * + * @param newProposedFederation the new proposed federation after the change. + * This can be {@code null}; the proposed federation is not always present. + */ + void onProposedFederationChange(Federation newProposedFederation); +} diff --git a/src/main/java/co/rsk/federate/watcher/FederationWatcherListenerImpl.java b/src/main/java/co/rsk/federate/watcher/FederationWatcherListenerImpl.java new file mode 100644 index 000000000..c2e4d3dbb --- /dev/null +++ b/src/main/java/co/rsk/federate/watcher/FederationWatcherListenerImpl.java @@ -0,0 +1,103 @@ +package co.rsk.federate.watcher; + +import co.rsk.federate.BtcToRskClient; +import co.rsk.federate.bitcoin.BitcoinWrapper; +import co.rsk.federate.btcreleaseclient.BtcReleaseClient; +import co.rsk.peg.federation.Federation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Objects; + +public class FederationWatcherListenerImpl implements FederationWatcherListener { + + private static final Logger logger = LoggerFactory.getLogger(FederationWatcherListenerImpl.class); + + private final BtcToRskClient btcToRskClientActive; + private final BtcToRskClient btcToRskClientRetiring; + private final BtcReleaseClient btcReleaseClient; + private final BitcoinWrapper bitcoinWrapper; + + public FederationWatcherListenerImpl( + BtcToRskClient btcToRskClientActive, + BtcToRskClient btcToRskClientRetiring, + BtcReleaseClient btcReleaseClient, + BitcoinWrapper bitcoinWrapper) { + this.btcToRskClientActive = btcToRskClientActive; + this.btcToRskClientRetiring = btcToRskClientRetiring; + this.btcReleaseClient = btcReleaseClient; + this.bitcoinWrapper = bitcoinWrapper; + } + + @Override + public void onActiveFederationChange(Federation newActiveFederation) { + triggerClientChange(btcToRskClientActive, newActiveFederation); + } + + @Override + public void onRetiringFederationChange(Federation newRetiringFederation) { + if (newRetiringFederation == null) { + clearRetiringFederationClient(); + return; + } + + triggerClientChange(btcToRskClientRetiring, newRetiringFederation); + } + + @Override + public void onProposedFederationChange(Federation newProposedFederation) { + if (newProposedFederation == null) { + logger.info( + "[onProposedFederationChange] Proposed federation was cleared"); + return; + } + + try { + // start {@code BtcReleaseClient} with proposed federation + // so it can sign svp spend tx + btcReleaseClient.start(newProposedFederation); + + // add proposed federation to active btc to rsk client so + // it can register svp spend tx in the bridge + bitcoinWrapper.addFederationListener(newProposedFederation, btcToRskClientActive); + + logger.info( + "[onProposedFederationChange] Clients for proposed federation [{}] started with success", + newProposedFederation.getAddress()); + } catch (Exception e) { + logger.error( + "[onProposedFederationChange] Clients for proposed federation [{}] failed to start", + newProposedFederation.getAddress(), + e); + } + } + + private void triggerClientChange(BtcToRskClient btcToRskClient, Federation newFederation) { + // This method assumes that the new federation cannot be null + Objects.requireNonNull(newFederation); + + try { + // Stop the current clients + btcToRskClient.stop(); + btcReleaseClient.stop(newFederation); + + // Start the current clients + btcToRskClient.start(newFederation); + btcReleaseClient.start(newFederation); + + logger.info( + "[triggerClientChange] Clients for federation [{}] changed with success", + newFederation.getAddress()); + } catch (Exception e) { + logger.error( + "[triggerClientChange] Clients for federation [{}] cannot be changed", + newFederation.getAddress(), + e); + } + } + + private void clearRetiringFederationClient() { + logger.info("[triggerClientChange] Clearing retiring federation client"); + + btcToRskClientRetiring.stop(); + } +} diff --git a/src/test/java/co/rsk/federate/BtcToRskClientTest.java b/src/test/java/co/rsk/federate/BtcToRskClientTest.java index c735db2e3..77ccc5ab6 100644 --- a/src/test/java/co/rsk/federate/BtcToRskClientTest.java +++ b/src/test/java/co/rsk/federate/BtcToRskClientTest.java @@ -57,7 +57,7 @@ class BtcToRskClientTest { private ActivationConfig activationConfig; private BridgeConstants bridgeRegTestConstants; private Federation activeFederation; - private FederationMember fakeMember; + private FederationMember activeFederationMember; private BtcToRskClientBuilder btcToRskClientBuilder; private List federationPrivateKeys; private NetworkParameters networkParameters; @@ -72,9 +72,7 @@ void setup() throws PeginInstructionsException, IOException { networkParameters = ThinConverter.toOriginalInstance(bridgeRegTestConstants.getBtcParamsString()); federationPrivateKeys = TestUtils.getFederationPrivateKeys(9); activeFederation = TestUtils.createFederation(bridgeRegTestConstants.getBtcParams(), federationPrivateKeys); - fakeMember = FederationMember.getFederationMemberFromKey( - BtcECKey.fromPrivate(HashUtil.keccak256("00".getBytes(StandardCharsets.UTF_8))) - ); + activeFederationMember = FederationMember.getFederationMemberFromKey(federationPrivateKeys.get(0)); btcToRskClientBuilder = new BtcToRskClientBuilder(); } @@ -94,7 +92,7 @@ void start_withNoFederationMember_doesntThrowError() throws Exception { BitcoinWrapper bw = new SimpleBitcoinWrapper(); SimpleFederatorSupport fh = new SimpleFederatorSupport(); - fh.setMember(fakeMember); + fh.setMember(activeFederationMember); BtcToRskClient client = createClientWithMocks(bw, fh); assertDoesNotThrow(() -> client.start(activeFederation)); } @@ -480,7 +478,7 @@ void onBlock_including_segwit_tx_registers_coinbase() throws Exception { when(mockedActivationConfig.isActive(eq(ConsensusRule.RSKIP143), anyLong())).thenReturn(true); FederatorSupport federatorSupport = mock(FederatorSupport.class); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BtcToRskClient client = spy(buildWithFactoryAndSetup( federatorSupport, @@ -1752,7 +1750,7 @@ void updateTransaction_with_release_before_rskip143() throws Exception { SimpleFederatorSupport federatorSupport = new SimpleFederatorSupport(); // set a fake member to federatorSupport to recreate a not-null runner - federatorSupport.setMember(fakeMember); + federatorSupport.setMember(activeFederationMember); BtcToRskClient client = buildWithFactoryAndSetup( federatorSupport, @@ -2242,7 +2240,7 @@ void updateBridgeBtcCoinbaseTransactions_when_coinbase_map_has_readyToBeInformed when(btcToRskClientFileStorageMock.read(any())).thenReturn(new BtcToRskClientFileReadResult(true, btcToRskClientFileData)); FederatorSupport federatorSupport = mock(FederatorSupport.class); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BtcToRskClient client = buildWithFactoryAndSetup( federatorSupport, @@ -2282,7 +2280,7 @@ void updateBridgeBtcCoinbaseTransactions_when_coinbase_map_has_readyToBeInformed FederatorSupport federatorSupport = mock(FederatorSupport.class); // mocking that the coinbase was already informed when(federatorSupport.hasBlockCoinbaseInformed(any())).thenReturn(true); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BtcToRskClient client = buildWithFactoryAndSetup( federatorSupport, @@ -2326,7 +2324,7 @@ void updateBridgeBtcCoinbaseTransactions_when_coinbase_map_has_readyToBeInformed FederatorSupport federatorSupport = mock(FederatorSupport.class); // Mocking the Bridge to indicate the coinbase was not informed, and then it was when(federatorSupport.hasBlockCoinbaseInformed(any())).thenReturn(false, true); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BtcToRskClient client = buildWithFactoryAndSetup( federatorSupport, @@ -2373,7 +2371,7 @@ void updateBridgeBtcCoinbaseTransactions_not_removing_from_storage_until_confirm FederatorSupport federatorSupport = mock(FederatorSupport.class); // mocking that the coinbase was not informed when(federatorSupport.hasBlockCoinbaseInformed(any())).thenReturn(false); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BtcToRskClient client = buildWithFactoryAndSetup( federatorSupport, @@ -2415,7 +2413,7 @@ void updateBridge_when_does_not_hasBetterBlockToSync_updates_headers_coinbase_tr FederatorSupport federatorSupport = mock(FederatorSupport.class); when(federatorSupport.getBtcBestBlockChainHeight()).thenReturn(1); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BitcoinWrapper bitcoinWrapper = mock(BitcoinWrapper.class); when(bitcoinWrapper.getBestChainHeight()).thenReturn(1); @@ -2447,7 +2445,7 @@ void updateBridge_whenUpdateBridgeConfigAreFalse_shouldNotCallAny() throws Excep FederatorSupport federatorSupport = mock(FederatorSupport.class); when(federatorSupport.getBtcBestBlockChainHeight()).thenReturn(1); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BitcoinWrapper bitcoinWrapper = mock(BitcoinWrapper.class); when(bitcoinWrapper.getBestChainHeight()).thenReturn(1); @@ -2481,7 +2479,7 @@ void updateBridge_noUpdateBridgeConfigDefined_shouldTriggerBridgeUpdates() throw FederatorSupport federatorSupport = mock(FederatorSupport.class); when(federatorSupport.getBtcBestBlockChainHeight()).thenReturn(1); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BitcoinWrapper bitcoinWrapper = mock(BitcoinWrapper.class); when(bitcoinWrapper.getBestChainHeight()).thenReturn(1); @@ -2525,7 +2523,7 @@ void updateBridgeBtcTransactions_tx_with_witness_already_informed() throws Excep when(federatorSupport.getBtcBestBlockChainHeight()).thenReturn(1); when(federatorSupport.isBtcTxHashAlreadyProcessed(peginTx.getTxId())).thenReturn(true); when(federatorSupport.getBtcTxHashProcessedHeight(peginTx.getTxId())).thenReturn(1L); - when(federatorSupport.getFederationMember()).thenReturn(fakeMember); + when(federatorSupport.getFederationMember()).thenReturn(activeFederationMember); BitcoinWrapper bitcoinWrapper = mock(BitcoinWrapper.class); when(bitcoinWrapper.getBestChainHeight()).thenReturn(1); @@ -2600,7 +2598,7 @@ private BtcToRskClient buildWithFactoryAndSetup( PowpegNodeSystemProperties config = nonNull(fedNodeSystemProperties) ? fedNodeSystemProperties : getMockedFedNodeSystemProperties(true); - if(MockUtil.isMock(config)) { + if (MockUtil.isMock(config)) { when(config.getActivationConfig()).thenReturn(activationConfig); } diff --git a/src/test/java/co/rsk/federate/FedNodeRunnerTest.java b/src/test/java/co/rsk/federate/FedNodeRunnerTest.java index 93ade6460..b4116c876 100644 --- a/src/test/java/co/rsk/federate/FedNodeRunnerTest.java +++ b/src/test/java/co/rsk/federate/FedNodeRunnerTest.java @@ -39,6 +39,7 @@ import co.rsk.federate.signing.hsm.client.HSMClientProtocolFactory; import co.rsk.federate.signing.hsm.message.PowHSMBlockchainParameters; import co.rsk.federate.signing.utils.TestUtils; +import co.rsk.federate.watcher.FederationWatcher; import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import java.io.IOException; diff --git a/src/test/java/co/rsk/federate/FederationProviderFromFederatorSupportTest.java b/src/test/java/co/rsk/federate/FederationProviderFromFederatorSupportTest.java index 60aa1788e..8d2ee535c 100644 --- a/src/test/java/co/rsk/federate/FederationProviderFromFederatorSupportTest.java +++ b/src/test/java/co/rsk/federate/FederationProviderFromFederatorSupportTest.java @@ -1,11 +1,14 @@ package co.rsk.federate; +import static co.rsk.peg.federation.FederationChangeResponseCode.FEDERATION_NON_EXISTENT; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP123; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP284; +import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP419; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -13,8 +16,9 @@ import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.bitcoinj.core.NetworkParameters; import co.rsk.bitcoinj.script.Script; +import co.rsk.federate.bitcoin.BitcoinTestUtils; +import co.rsk.peg.constants.BridgeMainNetConstants; import co.rsk.peg.federation.*; - import co.rsk.peg.federation.constants.FederationConstants; import co.rsk.peg.federation.constants.FederationTestNetConstants; import java.math.BigInteger; @@ -30,16 +34,15 @@ import org.junit.jupiter.api.Test; class FederationProviderFromFederatorSupportTest { - private FederatorSupport federatorSupportMock; - private FederationProvider federationProvider; - private FederationConstants federationConstants; - private NetworkParameters testnetParams; - private Instant creationTime; private static final int STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION = FederationFormatVersion.STANDARD_MULTISIG_FEDERATION.getFormatVersion(); private static final int NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION = FederationFormatVersion.NON_STANDARD_ERP_FEDERATION.getFormatVersion(); private static final int P2SH_ERP_FEDERATION_FORMAT_VERSION = FederationFormatVersion.P2SH_ERP_FEDERATION.getFormatVersion(); + private static final NetworkParameters NETWORK_PARAMETERS = BridgeMainNetConstants.getInstance().getBtcParams(); + private static final List KEYS = BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"k1", "k2", "k3"}, true); + private static final Address DEFAULT_ADDRESS = BitcoinTestUtils.createP2SHMultisigAddress(NETWORK_PARAMETERS, KEYS); + private static final Address HARDCODED_TESTNET_FED_ADDRESS = Address.fromBase58( NetworkParameters.fromID(NetworkParameters.ID_TESTNET), "2Mw6KM642fbkypTzbgFi6DTgTFPRWZUD4BA" @@ -48,6 +51,12 @@ class FederationProviderFromFederatorSupportTest { Hex.decode("6453210208f40073a9e43b3e9103acec79767a6de9b0409749884e989960fee578012fce210225e892391625854128c5c4ea4340de0c2a70570f33db53426fc9c746597a03f42102afc230c2d355b1a577682b07bc2646041b5d0177af0f98395a46018da699b6da210344a3c38cd59afcba3edcebe143e025574594b001700dec41e59409bdbd0f2a0921039a060badbeb24bee49eb2063f616c0f0f0765d4ca646b20a88ce828f259fcdb955670300cd50b27552210216c23b2ea8e4f11c3f9e22711addb1d16a93964796913830856b568cc3ea21d3210275562901dd8faae20de0a4166362a4f82188db77dbed4ca887422ea1ec185f1421034db69f2112f4fb1bb6141bf6e2bd6631f0484d0bd95b16767902c9fe219d4a6f5368ae") ); + private FederatorSupport federatorSupportMock; + private FederationProvider federationProvider; + private FederationConstants federationConstants; + private NetworkParameters testnetParams; + private Instant creationTime; + @BeforeEach void createProvider() { federationConstants = FederationTestNetConstants.getInstance(); @@ -57,7 +66,7 @@ void createProvider() { federationConstants ); testnetParams = NetworkParameters.fromID(NetworkParameters.ID_TESTNET); - creationTime = Instant.ofEpochMilli(5005L); + creationTime = Instant.ofEpochSecond(5); } @Test @@ -220,20 +229,20 @@ void getActiveFederationAddress() { @Test void getRetiringFederation_none() { - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(-1); + when(federatorSupportMock.getRetiringFederationSize()).thenReturn(FEDERATION_NON_EXISTENT.getCode()); assertEquals(Optional.empty(), federationProvider.getRetiringFederation()); - verify(federatorSupportMock, times(1)).getRetiringFederationSize(); - verify(federatorSupportMock, times(1)).getRetiringFederationAddress(); + verify(federatorSupportMock).getRetiringFederationSize(); } @Test - void getRetiringFederation_no_address() { - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(5); - - assertEquals(Optional.empty(), federationProvider.getRetiringFederation()); - verify(federatorSupportMock, times(1)).getRetiringFederationSize(); - verify(federatorSupportMock, times(1)).getRetiringFederationAddress(); + void getRetiringFederation_whenAddressNotPresent_shouldThrowIllegalStateException() { + // Arrange + when(federatorSupportMock.getRetiringFederationSize()).thenReturn(3); + when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.empty()); // Address is missing + + // Act & Assert + assertThrows(IllegalStateException.class, () -> federationProvider.getRetiringFederation()); } @Test @@ -272,7 +281,7 @@ void getRetiringFederation_present_afterMultikey() { when(configMock.isActive(RSKIP123)).thenReturn(true); Federation expectedFederation = createFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000, 12000) + getFederationMembersFromPks(1, 2000, 4000, 6000, 8000, 10000, 12000) ); Address expectedFederationAddress = expectedFederation.getAddress(); @@ -364,442 +373,132 @@ void getRetiringFederation_present_p2sh_erp_federation() { } @Test - void getLiveFederations_onlyActive_beforeMultikey() { - ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(false); - - Federation expectedActiveFederation = createFederation( - getFederationMembersFromPks(0, 1000, 2000, 3000, 4000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKey(i)).thenReturn(BtcECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - } - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(-1); - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(1, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - } - - @Test - void getLiveFederations_onlyActive_afterMultikey() { + void getProposedFederation_whenProposedFederationSizeIsNonExistent_shouldReturnEmptyOptional() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createFederation( - getFederationMembersFromPks(1,1000, 2000, 3000, 4000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(true); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(-1); + when(federatorSupportMock.getProposedFederationSize()) + .thenReturn(Optional.of(FEDERATION_NON_EXISTENT.getCode())); - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(1, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); + // Act & Assert + assertEquals(Optional.empty(), federationProvider.getProposedFederation()); + verify(federatorSupportMock).getProposedFederationSize(); } @Test - void getLiveFederations_onlyActive_erp_federation() { + void getProposedFederation_whenSomeDataDoesNotExists_shouldThrowIllegalStateException() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createNonStandardErpFederation( - getFederationMembersFromPks(1,1000, 2000, 3000, 4000), - configMock - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(true); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(-1); - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(1, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - } - - @Test - void getLiveFederations_onlyActive_p2sh_erp_federation() { - ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createP2shErpFederation( - getFederationMembersFromPks(1,1000, 2000, 3000, 4000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(-1); - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(1, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(P2SH_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - } - - @Test - void getLiveFederations_both_beforeMultikey() { - ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(false); - - Federation expectedActiveFederation = createFederation( - getFederationMembersFromPks(0,1000, 2000, 3000, 4000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createFederation( - getFederationMembersFromPks(0, 2000, 4000, 6000, 8000, 10000, 12000) - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - - when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); + Federation expectedFederation = createP2shErpFederation( + getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000)); + Address expectedFederationAddress = expectedFederation.getAddress(); + Integer federationSize = 5; + when(federatorSupportMock.getProposedFederationSize()).thenReturn(Optional.of(federationSize)); + when(federatorSupportMock.getProposedFederationCreationTime()).thenReturn(Optional.of(creationTime)); + when(federatorSupportMock.getProposedFederationAddress()).thenReturn(Optional.of(expectedFederationAddress)); when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKey(i)).thenReturn(BtcECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - } + when(federatorSupportMock.getProposedFederationCreationBlockNumber()).thenReturn(Optional.of(0L)); + when(federatorSupportMock.getProposedFederatorPublicKeyOfType(0, FederationMember.KeyType.BTC)) + .thenReturn(Optional.empty()); - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(6); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 6; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKey(i)).thenReturn(BtcECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - } - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - - Federation retiringFederation = liveFederations.get(1); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Act & Assert + assertThrows(IllegalStateException.class, () -> federationProvider.getProposedFederation()); } @Test - void getLiveFederations_both_afterMultikey() { + void getProposedFederation_whenExistsAndIsP2shErpFederation_shouldReturnProposedFederation() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createFederation( - getFederationMembersFromPks(1,1000, 2000, 3000, 4000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000, 12000) - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(true); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(4); - when(federatorSupportMock.getFederationThreshold()).thenReturn(2); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 4; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(6); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); + Federation expectedFederation = createP2shErpFederation( + getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000)); + Address expectedFederationAddress = expectedFederation.getAddress(); + Integer federationSize = 5; + when(federatorSupportMock.getProposedFederationSize()).thenReturn(Optional.of(federationSize)); + when(federatorSupportMock.getProposedFederationCreationTime()).thenReturn(Optional.of(creationTime)); + when(federatorSupportMock.getProposedFederationAddress()).thenReturn(Optional.of(expectedFederationAddress)); when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 6; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+1))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+2))); + when(federatorSupportMock.getProposedFederationCreationBlockNumber()).thenReturn(Optional.of(0L)); + for (int i = 0; i < federationSize; i++) { + when(federatorSupportMock.getProposedFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)) + .thenReturn(Optional.of(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000)))); + when(federatorSupportMock.getProposedFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)) + .thenReturn(Optional.of(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1)))); + when(federatorSupportMock.getProposedFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)) + .thenReturn(Optional.of(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2)))); } - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); - - Federation activeFederation = liveFederations.get(0); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); + // Act + Optional proposedFederation = federationProvider.getProposedFederation(); - Federation retiringFederation = liveFederations.get(1); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Assert + assertTrue(proposedFederation.isPresent()); + assertEquals(P2SH_ERP_FEDERATION_FORMAT_VERSION, proposedFederation.get().getFormatVersion()); + assertEquals(expectedFederation, proposedFederation.get()); + assertEquals(expectedFederationAddress, proposedFederation.get().getAddress()); } @Test - void getLiveFederations_both_erp_federations() { + void getProposedFederation_whenRSKIP419IsNotActivated_shouldReturnEmptyOptional() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createNonStandardErpFederation( - getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000), - configMock - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createNonStandardErpFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000), - configMock - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(false); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(5); - when(federatorSupportMock.getFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(5); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+1))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+2))); - } - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); + // Act + Optional result = federationProvider.getProposedFederation(); - Federation activeFederation = liveFederations.get(0); - assertEquals(NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - - Federation retiringFederation = liveFederations.get(1); - assertEquals(NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Assert + assertFalse(result.isPresent()); } @Test - void getLiveFederations_retiring_multikey_active_erp() { + void getProposedFederationAddress_whenAddressExists_shouldReturnAddress() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createNonStandardErpFederation( - getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000), - configMock - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000) - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(true); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(5); - when(federatorSupportMock.getFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } + when(federatorSupportMock.getProposedFederationAddress()).thenReturn(Optional.of(DEFAULT_ADDRESS)); - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(5); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+1))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+2))); - } - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); + // Act + Optional
result = federationProvider.getProposedFederationAddress(); - Federation activeFederation = liveFederations.get(0); - assertEquals(NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - - Federation retiringFederation = liveFederations.get(1); - assertEquals(STANDARD_MULTISIG_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Assert + assertTrue(result.isPresent()); + assertEquals(DEFAULT_ADDRESS, result.get()); } @Test - void getLiveFederations_retiring_erp_active_p2sh_erp() { + void getProposedFederationAddress_whenNoAddressExists_shouldReturnEmptyOptional() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createP2shErpFederation( - getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createNonStandardErpFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000), - configMock - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(true); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(5); - when(federatorSupportMock.getFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } + when(federatorSupportMock.getProposedFederationAddress()).thenReturn(Optional.empty()); - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(5); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+1))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+2))); - } - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); + // Act + Optional
result = federationProvider.getProposedFederationAddress(); - Federation activeFederation = liveFederations.get(0); - assertEquals(P2SH_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); - - Federation retiringFederation = liveFederations.get(1); - assertEquals(NON_STANDARD_ERP_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Assert + assertFalse(result.isPresent()); } - + @Test - void getLiveFederations_both_p2sh_erp_federations() { + void getProposedFederationAddress_whenRSKIP419IsNotActivated_shouldReturnEmptyOptional() { + // Arrange ActivationConfig.ForBlock configMock = mock(ActivationConfig.ForBlock.class); - when(configMock.isActive(RSKIP123)).thenReturn(true); - - Federation expectedActiveFederation = createP2shErpFederation( - getFederationMembersFromPks(1, 1000, 2000, 3000, 4000, 5000) - ); - Address expectedActiveFederationAddress = expectedActiveFederation.getAddress(); - - Federation expectedRetiringFederation = createP2shErpFederation( - getFederationMembersFromPks(1,2000, 4000, 6000, 8000, 10000) - ); - Address expectedRetiringFederationAddress = expectedRetiringFederation.getAddress(); - + when(configMock.isActive(RSKIP419)).thenReturn(false); when(federatorSupportMock.getConfigForBestBlock()).thenReturn(configMock); - when(federatorSupportMock.getFederationSize()).thenReturn(5); - when(federatorSupportMock.getFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getFederationAddress()).thenReturn(expectedActiveFederationAddress); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+1))); - when(federatorSupportMock.getFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*1000+2))); - } - - when(federatorSupportMock.getRetiringFederationSize()).thenReturn(5); - when(federatorSupportMock.getRetiringFederationThreshold()).thenReturn(3); - when(federatorSupportMock.getRetiringFederationCreationTime()).thenReturn(creationTime); - when(federatorSupportMock.getRetiringFederationAddress()).thenReturn(Optional.of(expectedRetiringFederationAddress)); - when(federatorSupportMock.getBtcParams()).thenReturn(testnetParams); - for (int i = 0; i < 5; i++) { - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.BTC)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.RSK)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+1))); - when(federatorSupportMock.getRetiringFederatorPublicKeyOfType(i, FederationMember.KeyType.MST)).thenReturn(ECKey.fromPrivate(BigInteger.valueOf((i+1)*2000+2))); - } - - List liveFederations = federationProvider.getLiveFederations(); - assertEquals(2, liveFederations.size()); - Federation activeFederation = liveFederations.get(0); - assertEquals(P2SH_ERP_FEDERATION_FORMAT_VERSION, activeFederation.getFormatVersion()); - assertEquals(expectedActiveFederation, activeFederation); - assertEquals(expectedActiveFederationAddress, activeFederation.getAddress()); + // Act + Optional
result = federationProvider.getProposedFederationAddress(); - Federation retiringFederation = liveFederations.get(1); - assertEquals(P2SH_ERP_FEDERATION_FORMAT_VERSION, retiringFederation.getFormatVersion()); - assertEquals(expectedRetiringFederation, retiringFederation); - assertEquals(expectedRetiringFederationAddress, retiringFederation.getAddress()); + // Assert + assertFalse(result.isPresent()); } private Federation createFederation(List members) { diff --git a/src/test/java/co/rsk/federate/FederationWatcherTest.java b/src/test/java/co/rsk/federate/FederationWatcherTest.java deleted file mode 100644 index 7c9119778..000000000 --- a/src/test/java/co/rsk/federate/FederationWatcherTest.java +++ /dev/null @@ -1,375 +0,0 @@ -package co.rsk.federate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import co.rsk.bitcoinj.core.BtcECKey; -import co.rsk.bitcoinj.core.NetworkParameters; -import co.rsk.federate.signing.utils.TestUtils; -import co.rsk.peg.federation.Federation; -import co.rsk.peg.federation.FederationArgs; -import co.rsk.peg.federation.FederationFactory; -import co.rsk.peg.federation.FederationMember; -import java.math.BigInteger; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import org.ethereum.crypto.ECKey; -import org.ethereum.facade.Ethereum; -import org.ethereum.listener.EthereumListenerAdapter; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; - -class FederationWatcherTest { - List federation1Members = getFederationMembersFromPksForBtc(1000, 2000, 3000, 4000); - Instant federation1CreationTime = Instant.ofEpochMilli(5005L); - long creationBlockNumber = 0L; - NetworkParameters btcParams = NetworkParameters.fromID(NetworkParameters.ID_REGTEST); - - FederationArgs federation1Args = new FederationArgs(federation1Members, federation1CreationTime, creationBlockNumber, btcParams); - private final Federation federation1 = FederationFactory.buildStandardMultiSigFederation(federation1Args); - - List federation2Members = getFederationMembersFromPksForBtc(2000, 3000, 4000, 5000, 6000, 7000); - Instant federation2CreationTime = Instant.ofEpochMilli(15300L); - FederationArgs federation2Args = new FederationArgs(federation2Members, federation2CreationTime, creationBlockNumber, btcParams); - private final Federation federation2 = FederationFactory.buildStandardMultiSigFederation(federation2Args); - - List federation3Members = getFederationMembersFromPksForBtc(5000, 6000, 7000); - Instant federation3CreationTime = Instant.ofEpochMilli(7400L); - FederationArgs federation3Args = new FederationArgs(federation3Members, federation3CreationTime, creationBlockNumber, btcParams); - private final Federation federation3 = FederationFactory.buildStandardMultiSigFederation(federation3Args); - - private FederationProvider federationProvider; - private Ethereum ethereumMock; - private FederationWatcher watcher; - - @BeforeEach - void createMocksAndWatcher() { - federationProvider = mock(FederationProvider.class); - ethereumMock = mock(Ethereum.class); - watcher = new FederationWatcher(ethereumMock); - } - - @Test - void setsListenerUp() throws Exception { - Mockito.doAnswer((InvocationOnMock m) -> { - Object listener = m.getArgument(0); - assertEquals("co.rsk.federate.FederationWatcher$FederationWatcherRskListener", listener.getClass().getName()); - assertSame(TestUtils.getInternalState(watcher, "federationProvider"), federationProvider); - return null; - }).when(ethereumMock).addListener(any()); - - watcher.setup(federationProvider); - verify(ethereumMock).addListener(any()); - } - - @Test - void triggersActiveFederationChange_none_to_active() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.empty(), Optional.empty()); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation1.getAddress()); - when(federationProvider.getActiveFederation()).thenReturn(federation1); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - assertEquals(Optional.empty(), oldFederation); - assertEquals(federation1, newFederation); - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(2, logger.activeCalls); - assertEquals(0, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, times(1)).getActiveFederation(); - verify(federationProvider, never()).getRetiringFederation(); - } - - @Test - void triggersActiveFederationChange_active_to_otherActive() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation1), Optional.empty()); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation2.getAddress()); - when(federationProvider.getActiveFederation()).thenReturn(federation2); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - assertEquals(Optional.of(federation1), oldFederation); - assertEquals(federation2, newFederation); - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(2, logger.activeCalls); - assertEquals(0, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, times(1)).getActiveFederation(); - verify(federationProvider, never()).getRetiringFederation(); - } - - @Test - void doesntTriggerActiveOrRetiringFederationChange_none() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation1), Optional.empty()); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation1.getAddress()); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(0, logger.activeCalls); - assertEquals(0, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, never()).getActiveFederation(); - verify(federationProvider, never()).getRetiringFederation(); - } - - @Test - void doesntTriggerActiveOrRetiringFederationChange_noChange() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation1), Optional.of(federation2)); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation1.getAddress()); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(federation2.getAddress())); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(0, logger.activeCalls); - assertEquals(0, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, never()).getActiveFederation(); - verify(federationProvider, never()).getRetiringFederation(); - } - - @Test - void triggersRetiringFederationChange_none_to_retiring() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation2), Optional.empty()); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation2.getAddress()); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(federation1.getAddress())); - when(federationProvider.getRetiringFederation()).thenReturn(Optional.of(federation1)); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - assertEquals(Optional.empty(), oldFederation); - assertEquals(Optional.of(federation1), newFederation); - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(0, logger.activeCalls); - assertEquals(2, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, never()).getActiveFederation(); - verify(federationProvider, times(1)).getRetiringFederation(); - } - - @Test - void triggersRetiringFederationChange_retiring_to_none() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation2), Optional.of(federation1)); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation2.getAddress()); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); - when(federationProvider.getRetiringFederation()).thenReturn(Optional.empty()); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - assertEquals(Optional.of(federation1), oldFederation); - assertEquals(Optional.empty(), newFederation); - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(0, logger.activeCalls); - assertEquals(2, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, never()).getActiveFederation(); - verify(federationProvider, times(1)).getRetiringFederation(); - } - - @Test - void triggersRetiringFederationChange_retiring_to_otherRetiring() throws Exception { - EthereumListenerAdapter rskListener = setupAndGetRskListener(Optional.of(federation3), Optional.of(federation1)); - class EventsLogger - { - public int activeCalls = 0; - public int retiringCalls = 0; - } - - when(federationProvider.getActiveFederationAddress()).thenReturn(federation3.getAddress()); - when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(federation2.getAddress())); - when(federationProvider.getRetiringFederation()).thenReturn(Optional.of(federation2)); - - EventsLogger logger = new EventsLogger(); - - for (int i = 0; i < 2; i++) { - watcher.addListener(new FederationWatcher.Listener() { - @Override - public void onActiveFederationChange(Optional oldFederation, Federation newFederation) { - logger.activeCalls++; - } - - @Override - public void onRetiringFederationChange(Optional oldFederation, Optional newFederation) { - assertEquals(Optional.of(federation1), oldFederation); - assertEquals(Optional.of(federation2), newFederation); - logger.retiringCalls++; - } - }); - } - - rskListener.onBestBlock(null, null); - assertEquals(0, logger.activeCalls); - assertEquals(2, logger.retiringCalls); - verify(federationProvider, times(1)).getActiveFederationAddress(); - verify(federationProvider, times(1)).getRetiringFederationAddress(); - verify(federationProvider, never()).getActiveFederation(); - verify(federationProvider, times(1)).getRetiringFederation(); - } - - private EthereumListenerAdapter setupAndGetRskListener(Optional activeFederation, Optional retiringFederation) throws Exception { - class ListenerHolder { - public EthereumListenerAdapter listener = null; - } - - final ListenerHolder holder = new ListenerHolder(); - Mockito.doAnswer((InvocationOnMock m) -> { - holder.listener = m.getArgument(0); - return null; - }).when(ethereumMock).addListener(any()); - watcher.setup(federationProvider); - TestUtils.setInternalState(watcher, "activeFederation", activeFederation); - TestUtils.setInternalState(watcher, "retiringFederation", retiringFederation); - assertNotNull(holder.listener); - return holder.listener; - } - - private List getFederationMembersFromPksForBtc(Integer... pks) { - return Arrays.stream(pks).map(n -> new FederationMember( - BtcECKey.fromPrivate(BigInteger.valueOf(n)), - new ECKey(), - new ECKey() - )).collect(Collectors.toList()); - } -} diff --git a/src/test/java/co/rsk/federate/FederatorSupportTest.java b/src/test/java/co/rsk/federate/FederatorSupportTest.java index a9701b960..ddcbd0e0b 100644 --- a/src/test/java/co/rsk/federate/FederatorSupportTest.java +++ b/src/test/java/co/rsk/federate/FederatorSupportTest.java @@ -1,27 +1,39 @@ package co.rsk.federate; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import co.rsk.peg.constants.BridgeRegTestConstants; +import co.rsk.peg.constants.BridgeMainNetConstants; +import co.rsk.peg.federation.FederationMember; +import co.rsk.bitcoinj.core.Address; +import co.rsk.bitcoinj.core.BtcECKey; +import co.rsk.bitcoinj.core.BtcTransaction; +import co.rsk.bitcoinj.core.NetworkParameters; +import co.rsk.crypto.Keccak256; import co.rsk.federate.adapter.ThinConverter; +import co.rsk.federate.bitcoin.BitcoinTestUtils; import co.rsk.federate.config.TestSystemProperties; +import co.rsk.federate.signing.utils.TestUtils; import co.rsk.peg.Bridge; import co.rsk.peg.BridgeMethods; +import co.rsk.peg.StateForProposedFederator; +import java.util.AbstractMap; +import java.math.BigInteger; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.bitcoinj.core.Address; +import java.util.Map; +import java.util.Optional; import org.bitcoinj.core.Block; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.PartialMerkleTree; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.TransactionInput; @@ -29,15 +41,32 @@ import org.bitcoinj.core.TransactionWitness; import org.bouncycastle.util.encoders.Hex; import org.ethereum.core.Blockchain; +import org.ethereum.crypto.ECKey; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; class FederatorSupportTest { - private final NetworkParameters networkParameters = ThinConverter.toOriginalInstance(new BridgeRegTestConstants().getBtcParamsString()); + private static final NetworkParameters NETWORK_PARAMETERS = BridgeMainNetConstants.getInstance().getBtcParams(); + private static final List KEYS = BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"k1", "k2", "k3"}, true); + private static final Address DEFAULT_ADDRESS = BitcoinTestUtils.createP2SHMultisigAddress(NETWORK_PARAMETERS, KEYS); + private static final byte[] PUBLIC_KEY = ECKey.fromPrivate(BigInteger.valueOf(100)).getPubKey(); + + private BridgeTransactionSender bridgeTransactionSender; + private FederatorSupport federatorSupport; + + @BeforeEach + void setup() { + bridgeTransactionSender = mock(BridgeTransactionSender.class); + federatorSupport = new FederatorSupport( + mock(Blockchain.class), new TestSystemProperties(), bridgeTransactionSender); + } @Test void sendReceiveHeadersSendsBlockHeaders() { + org.bitcoinj.core.NetworkParameters networkParameters = + ThinConverter.toOriginalInstance(BridgeMainNetConstants.getInstance().getBtcParamsString()); BridgeTransactionSender bridgeTransactionSender = mock(BridgeTransactionSender.class); FederatorSupport instance = new FederatorSupport( @@ -67,6 +96,8 @@ void sendReceiveHeadersSendsBlockHeaders() { @Test void sendRegisterCoinbaseTransaction() throws Exception { BridgeTransactionSender bridgeTransactionSender = mock(BridgeTransactionSender.class); + org.bitcoinj.core.NetworkParameters networkParameters = + ThinConverter.toOriginalInstance(BridgeMainNetConstants.getInstance().getBtcParamsString()); FederatorSupport fs = new FederatorSupport( mock(Blockchain.class), @@ -81,7 +112,7 @@ void sendRegisterCoinbaseTransaction() throws Exception { input.setWitness(witness); coinbaseTx.addInput(input); TransactionOutput output = new TransactionOutput(networkParameters, null, Coin.COIN, - Address.fromString(networkParameters, "mvbnrCX3bg1cDRUu8pkecrvP6vQkSLDSou")); + org.bitcoinj.core.Address.fromString(networkParameters, DEFAULT_ADDRESS.toBase58())); coinbaseTx.addOutput(output); List hashes = Collections.singletonList(Sha256Hash.ZERO_HASH); @@ -123,6 +154,311 @@ void hasBlockCoinbaseInformed() { assertTrue(fs.hasBlockCoinbaseInformed(Sha256Hash.ZERO_HASH)); } + @Test + void getStateForProposedFederator_whenCallTxReturnsNull_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_STATE_FOR_SVP_CLIENT))) + .thenReturn(null); + + // Act + Optional result = federatorSupport.getStateForProposedFederator(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getStateForProposedFederator_whenCallTxReturnsValidData_shouldReturnStateForProposedFederator() { + // Arrange + Keccak256 rskTxHash = TestUtils.createHash(1); + BtcTransaction btcTx = new BtcTransaction(NETWORK_PARAMETERS); + Map.Entry svpSpendTx = new AbstractMap.SimpleEntry<>(rskTxHash, btcTx); + StateForProposedFederator stateForProposedFederator = new StateForProposedFederator(svpSpendTx); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_STATE_FOR_SVP_CLIENT))) + .thenReturn(stateForProposedFederator.encodeToRlp()); + + // Act + Optional result = federatorSupport.getStateForProposedFederator(); + + // Assert + assertTrue(result.isPresent()); + assertEquals(svpSpendTx, result.get().getSvpSpendTxWaitingForSignatures()); + } + + @Test + void getProposedFederationAddress_whenAddressStringIsEmpty_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_ADDRESS))) + .thenReturn(""); + + // Act + Optional
result = federatorSupport.getProposedFederationAddress(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederationAddress_whenAddressStringIsNull_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_ADDRESS))) + .thenReturn(null); + + // Act + Optional
result = federatorSupport.getProposedFederationAddress(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederationAddress_whenAddressStringIsValid_shouldReturnAddress() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_ADDRESS))) + .thenReturn(DEFAULT_ADDRESS.toBase58()); + + // Act + Optional
result = federatorSupport.getProposedFederationAddress(); + + // Assert + assertTrue(result.isPresent()); + assertEquals(DEFAULT_ADDRESS.toString(), result.get().toString()); + } + + @Test + void getProposedFederationSize_whenSizeIsNull_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_SIZE))) + .thenReturn(null); + + // Act + Optional result = federatorSupport.getProposedFederationSize(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederationSize_whenSizeIsPresent_shouldReturnInteger() { + // Arrange + BigInteger expectedSize = BigInteger.valueOf(9); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_SIZE))) + .thenReturn(expectedSize); + + // Act + Optional result = federatorSupport.getProposedFederationSize(); + + // Assert + assertTrue(result.isPresent()); + assertEquals(expectedSize.intValue(), result.get()); + } + + @Test + void getProposedFederatorPublicKeyOfType_whenPublicKeyIsNull_shouldReturnEmptyOptional() { + // Arrange + int index = 0; + FederationMember.KeyType keyType = FederationMember.KeyType.BTC; + when(bridgeTransactionSender.callTx( + any(), + eq(Bridge.GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE), + eq(new Object[]{ index, keyType.getValue() }))) + .thenReturn(null); + + // Act + Optional result = federatorSupport.getProposedFederatorPublicKeyOfType(index, keyType); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederatorPublicKeyOfType_whenPublicKeyIsPresent_shouldReturnECKey() { + // Arrange + int index = 0; + FederationMember.KeyType keyType = FederationMember.KeyType.BTC; + ECKey expectedKey = ECKey.fromPublicOnly(PUBLIC_KEY); + when(bridgeTransactionSender.callTx( + any(), + eq(Bridge.GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE), + eq(new Object[]{ index, keyType.getValue() }))) + .thenReturn(PUBLIC_KEY); + + // Act + Optional result = federatorSupport.getProposedFederatorPublicKeyOfType(index, keyType); + + // Assert + assertTrue(result.isPresent()); + assertArrayEquals(expectedKey.getPubKey(), result.get().getPubKey()); + } + + @Test + void getProposedFederatorPublicKeyOfType_whenKeyTypeIsNull_shouldThrowNullPointerException() { + // Arrange + int index = 0; + + // Act & Assert + assertThrows(NullPointerException.class, () -> { + federatorSupport.getProposedFederatorPublicKeyOfType(index, null); + }); + } + + @Test + void getProposedFederationCreationTime_whenCreationTimeIsNull_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_CREATION_TIME))) + .thenReturn(null); + + // Act + Optional result = federatorSupport.getProposedFederationCreationTime(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederationCreationTime_whenCreationTimeIsValid_shouldReturnInstant() { + // Arrange + BigInteger expectedCreationTime = BigInteger.valueOf(System.currentTimeMillis()); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_CREATION_TIME))) + .thenReturn(expectedCreationTime); + + // Act + Optional result = federatorSupport.getProposedFederationCreationTime(); + + // Assert + assertTrue(result.isPresent()); + assertEquals(expectedCreationTime.longValue(), result.get().getEpochSecond()); + } + + @Test + void getProposedFederationCreationBlockNumber_whenBlockNumberIsNull_shouldReturnEmptyOptional() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER))) + .thenReturn(null); + + // Act + Optional result = federatorSupport.getProposedFederationCreationBlockNumber(); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getProposedFederationCreationBlockNumber_whenBlockNumberIsValid_shouldReturnLong() { + // Arrange + BigInteger expectedBlockNumber = BigInteger.valueOf(123456); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER))) + .thenReturn(expectedBlockNumber); + + // Act + Optional result = federatorSupport.getProposedFederationCreationBlockNumber(); + + // Assert + assertTrue(result.isPresent()); + assertEquals(expectedBlockNumber.longValue(), result.get()); + } + + @Test + void getActiveFederationCreationTime_whenCreationTimeIsValid_preRSKIP419_shouldReturnInstantFromMillis() { + // Arrange + BigInteger expectedCreationTime = BigInteger.valueOf(System.currentTimeMillis()); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_FEDERATION_CREATION_TIME))) + .thenReturn(expectedCreationTime); + + // all rskips are activated since block 0 + org.ethereum.core.Block mockBlock = mock(org.ethereum.core.Block.class); + when(mockBlock.getNumber()).thenReturn(-1L); + Blockchain blockchain = mock(Blockchain.class); + when(blockchain.getBestBlock()).thenReturn(mockBlock); + + federatorSupport = new FederatorSupport(blockchain, new TestSystemProperties(), bridgeTransactionSender); + + // Act + Instant result = federatorSupport.getFederationCreationTime(); + + // Assert + assertEquals(expectedCreationTime.longValue(), result.toEpochMilli()); + } + + @Test + void getActiveFederationCreationTime_whenCreationTimeIsValid_postRSKIP419_shouldReturnInstantFromSeconds() { + // Arrange + BigInteger expectedCreationTime = BigInteger.valueOf(System.currentTimeMillis()); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_FEDERATION_CREATION_TIME))) + .thenReturn(expectedCreationTime); + + // all rskips are activated since block 0 + org.ethereum.core.Block mockBlock = mock(org.ethereum.core.Block.class); + when(mockBlock.getNumber()).thenReturn(0L); + Blockchain blockchain = mock(Blockchain.class); + when(blockchain.getBestBlock()).thenReturn(mockBlock); + + federatorSupport = new FederatorSupport(blockchain, new TestSystemProperties(), bridgeTransactionSender); + + // Act + Instant result = federatorSupport.getFederationCreationTime(); + + // Assert + assertEquals(expectedCreationTime.longValue(), result.getEpochSecond()); + } + + @Test + void getRetiringFederationCreationTime_whenCreationTimeIsNull_shouldReturnNull() { + // Arrange + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_RETIRING_FEDERATION_CREATION_TIME))) + .thenReturn(null); + + // Act + Instant result = federatorSupport.getRetiringFederationCreationTime(); + + // Assert + assertNull(result); + } + + @Test + void getRetiringFederationCreationTime_whenCreationTimeIsValid_preRSKIP419_shouldReturnInstantFromMillis() { + // Arrange + BigInteger expectedCreationTime = BigInteger.valueOf(System.currentTimeMillis()); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_RETIRING_FEDERATION_CREATION_TIME))) + .thenReturn(expectedCreationTime); + + // all rskips are activated since block 0 + org.ethereum.core.Block mockBlock = mock(org.ethereum.core.Block.class); + when(mockBlock.getNumber()).thenReturn(-1L); + Blockchain blockchain = mock(Blockchain.class); + when(blockchain.getBestBlock()).thenReturn(mockBlock); + + federatorSupport = new FederatorSupport(blockchain, new TestSystemProperties(), bridgeTransactionSender); + + // Act + Instant result = federatorSupport.getRetiringFederationCreationTime(); + + // Assert + assertEquals(expectedCreationTime.longValue(), result.toEpochMilli()); + } + + @Test + void getRetiringFederationCreationTime_whenCreationTimeIsValid_postRSKIP419_shouldReturnInstantFromSeconds() { + // Arrange + BigInteger expectedCreationTime = BigInteger.valueOf(System.currentTimeMillis()); + when(bridgeTransactionSender.callTx(any(), eq(Bridge.GET_RETIRING_FEDERATION_CREATION_TIME))) + .thenReturn(expectedCreationTime); + + // all rskips are activated since block 0 + org.ethereum.core.Block mockBlock = mock(org.ethereum.core.Block.class); + when(mockBlock.getNumber()).thenReturn(0L); + Blockchain blockchain = mock(Blockchain.class); + when(blockchain.getBestBlock()).thenReturn(mockBlock); + + federatorSupport = new FederatorSupport(blockchain, new TestSystemProperties(), bridgeTransactionSender); + + // Act + Instant result = federatorSupport.getRetiringFederationCreationTime(); + + // Assert + assertEquals(expectedCreationTime.longValue(), result.getEpochSecond()); + } + private Sha256Hash createHash() { byte[] bytes = new byte[32]; bytes[0] = (byte) 1; diff --git a/src/test/java/co/rsk/federate/bitcoin/BitcoinTestUtils.java b/src/test/java/co/rsk/federate/bitcoin/BitcoinTestUtils.java index 4eeaf92c0..968e00ce7 100644 --- a/src/test/java/co/rsk/federate/bitcoin/BitcoinTestUtils.java +++ b/src/test/java/co/rsk/federate/bitcoin/BitcoinTestUtils.java @@ -5,6 +5,8 @@ import co.rsk.bitcoinj.core.Coin; import co.rsk.bitcoinj.core.NetworkParameters; import co.rsk.bitcoinj.core.Sha256Hash; +import co.rsk.bitcoinj.script.Script; +import co.rsk.bitcoinj.script.ScriptBuilder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -31,9 +33,32 @@ public static Sha256Hash createHash(int nHash) { return Sha256Hash.wrap(bytes); } + public static BtcECKey getBtcEcKeyFromSeed(String seed) { + byte[] serializedSeed = HashUtil.keccak256(seed.getBytes(StandardCharsets.UTF_8)); + return BtcECKey.fromPrivate(serializedSeed); + } + + public static List getBtcEcKeysFromSeeds(String[] seeds, boolean sorted) { + List keys = Arrays.stream(seeds) + .map(BitcoinTestUtils::getBtcEcKeyFromSeed) + .collect(Collectors.toList()); + + if (sorted) { + keys.sort(BtcECKey.PUBKEY_COMPARATOR); + } + + return keys; + } + public static Address createP2PKHAddress(NetworkParameters networkParameters, String seed) { BtcECKey key = BtcECKey.fromPrivate( HashUtil.keccak256(seed.getBytes(StandardCharsets.UTF_8))); return key.toAddress(networkParameters); } + + public static Address createP2SHMultisigAddress(NetworkParameters networkParameters, List keys) { + Script redeemScript = ScriptBuilder.createRedeemScript((keys.size() / 2) + 1, keys); + Script outputScript = ScriptBuilder.createP2SHOutputScript(redeemScript); + return Address.fromP2SHScript(networkParameters, outputScript); + } } diff --git a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientStorageSynchronizerTest.java b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientStorageSynchronizerTest.java index 4eb8bc252..5c2679542 100644 --- a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientStorageSynchronizerTest.java +++ b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientStorageSynchronizerTest.java @@ -186,13 +186,10 @@ void processBlock_ok() { TransactionReceipt receipt = mock(TransactionReceipt.class); List logs = new ArrayList<>(); - SignatureCache signatureCache = new BlockTxSignatureCache(new ReceivedTxSignatureCache()); - BridgeEventLoggerImpl bridgeEventLogger = new BridgeEventLoggerImpl( new BridgeRegTestConstants(), activations, - logs, - signatureCache + logs ); Keccak256 value = createHash(3); @@ -282,13 +279,10 @@ void accepts_transaction_with_two_release_requested() { TransactionReceipt receipt = mock(TransactionReceipt.class); List logs = new ArrayList<>(); - SignatureCache signatureCache = new BlockTxSignatureCache(new ReceivedTxSignatureCache()); - BridgeEventLoggerImpl bridgeEventLogger = new BridgeEventLoggerImpl( new BridgeRegTestConstants(), activations, - logs, - signatureCache + logs ); Keccak256 releaseRequestTxHash = createHash(3); diff --git a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java index d8d1603a9..27315a936 100644 --- a/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java +++ b/src/test/java/co/rsk/federate/btcreleaseclient/BtcReleaseClientTest.java @@ -28,7 +28,7 @@ import co.rsk.bitcoinj.core.Sha256Hash; import co.rsk.bitcoinj.core.TransactionInput; import co.rsk.bitcoinj.crypto.TransactionSignature; -import co.rsk.bitcoinj.params.RegTestParams; +import co.rsk.bitcoinj.params.MainNetParams; import co.rsk.bitcoinj.script.Script; import co.rsk.bitcoinj.script.ScriptBuilder; import co.rsk.bitcoinj.script.ScriptChunk; @@ -61,23 +61,26 @@ import co.rsk.net.NodeBlockProcessor; import co.rsk.peg.federation.*; import co.rsk.peg.StateForFederator; - +import co.rsk.peg.StateForProposedFederator; 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.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; - +import java.util.stream.Stream; +import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.ethereum.config.Constants; import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.core.Block; @@ -97,10 +100,13 @@ class BtcReleaseClientTest { - private final static Duration PEGOUT_SIGNED_CACHE_TTL = Duration.ofMinutes(30); + private static final Duration PEGOUT_SIGNED_CACHE_TTL = Duration.ofMinutes(30); - private NetworkParameters params; - private BridgeConstants bridgeConstants; + private final BlockStore blockStore = mock(BlockStore.class); + private final ReceiptStore receiptStore = mock(ReceiptStore.class); + private final Block bestBlock = mock(Block.class); + private final NetworkParameters params = MainNetParams.get(); + private final BridgeConstants bridgeConstants = Constants.mainnet().bridgeConstants; private static final List erpFedKeys = Arrays.stream(new String[]{ "03b9fc46657cf72a1afa007ecf431de1cd27ff5cc8829fa625b66ca47b967e6b24", @@ -110,15 +116,43 @@ class BtcReleaseClientTest { @BeforeEach void setup() { - params = RegTestParams.get(); - bridgeConstants = Constants.regtest().bridgeConstants; + Keccak256 blockHash = createHash(123); + when(bestBlock.getHash()).thenReturn(blockHash); + when(bestBlock.getNumber()).thenReturn(5_000L); + when(blockStore.getBlockByHash(blockHash.getBytes())) + .thenReturn(bestBlock); + } + + @Test + void start_whenFederationMemberNotPartOfDesiredFederation_shouldThrowException() { + // Arrange + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); + when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + FederatorSupport federatorSupport = mock(FederatorSupport.class); + Federation federation = TestUtils.createFederation(params, 1); + Federation otherFederation = TestUtils.createFederation(params, 2); + FederationMember federationMember = otherFederation.getMembers().get(1); + doReturn(federationMember).when(federatorSupport).getFederationMember(); + + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( + mock(Ethereum.class), + federatorSupport, + powpegNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + // Act & Assert + assertThrows(IllegalStateException.class, () -> btcReleaseClient.start(federation)); } @Test void if_start_not_called_rsk_blockchain_not_listened() { Ethereum ethereum = mock(Ethereum.class); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - Mockito.doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + Mockito.doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -136,21 +170,26 @@ void if_start_not_called_rsk_blockchain_not_listened() { void when_start_called_rsk_blockchain_is_listened() { Ethereum ethereum = mock(Ethereum.class); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - Mockito.doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + FederatorSupport federatorSupport = mock(FederatorSupport.class); + Mockito.doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); Federation fed1 = TestUtils.createFederation(params, 1); + FederationMember federationMember1 = fed1.getMembers().get(0); + doReturn(federationMember1).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed1); Federation fed2 = TestUtils.createFederation(params, 1); + FederationMember federationMember2 = fed2.getMembers().get(0); + doReturn(federationMember2).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed2); verify(ethereum, Mockito.times(1)).addListener(ArgumentMatchers.any(EthereumListener.class)); @@ -160,21 +199,26 @@ void when_start_called_rsk_blockchain_is_listened() { void if_stop_called_with_just_one_federation_rsk_blockchain_is_still_listened() { Ethereum ethereum = mock(Ethereum.class); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - Mockito.doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + FederatorSupport federatorSupport = mock(FederatorSupport.class); + Mockito.doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); - + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); Federation fed1 = TestUtils.createFederation(params, 1); + FederationMember federationMember1 = fed1.getMembers().get(0); + doReturn(federationMember1).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed1); Federation fed2 = TestUtils.createFederation(params, 1); + FederationMember federationMember2 = fed2.getMembers().get(0); + doReturn(federationMember2).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed2); Mockito.verify(ethereum, Mockito.times(1)).addListener(ArgumentMatchers.any(EthereumListener.class)); @@ -186,22 +230,27 @@ void if_stop_called_with_just_one_federation_rsk_blockchain_is_still_listened() @Test void if_stop_called_with_federations_rsk_blockchain_is_not_listened() { Ethereum ethereum = mock(Ethereum.class); + FederatorSupport federatorSupport = mock(FederatorSupport.class); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - Mockito.doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + Mockito.doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); BtcReleaseClient btcReleaseClient = new BtcReleaseClient( ethereum, - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); Federation fed1 = TestUtils.createFederation(params, 1); + FederationMember federationMember1 = fed1.getMembers().get(0); + doReturn(federationMember1).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed1); Federation fed2 = TestUtils.createFederation(params, 1); + FederationMember federationMember2 = fed2.getMembers().get(0); + doReturn(federationMember2).when(federatorSupport).getFederationMember(); btcReleaseClient.start(fed2); Mockito.verify(ethereum, Mockito.times(1)).addListener(ArgumentMatchers.any(EthereumListener.class)); @@ -215,6 +264,7 @@ void if_stop_called_with_federations_rsk_blockchain_is_not_listened() { void processReleases_ok() throws Exception { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); // Create a tx from the Fed to a random btc address BtcTransaction releaseTx = new BtcTransaction(params); @@ -235,7 +285,7 @@ void processReleases_ok() throws Exception { when(signer.sign(eq(BTC.getKeyId()), ArgumentMatchers.any())).thenReturn(ethSig); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -245,9 +295,12 @@ void processReleases_ok() throws Exception { .any(ReleaseCreationInformation.class))) .thenReturn(messageBuilder); + FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); + BtcReleaseClient client = new BtcReleaseClient( mock(Ethereum.class), - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); @@ -296,6 +349,7 @@ void processReleases_ok() throws Exception { void having_two_pegouts_signs_only_one() throws Exception { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); BtcTransaction tx1 = TestUtils.createBtcTransaction(params, federation); BtcTransaction tx2 = TestUtils.createBtcTransaction(params, federation); @@ -322,6 +376,7 @@ void having_two_pegouts_signs_only_one() throws Exception { any(byte[].class) ); doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + doReturn(federationMember).when(federatorSupport).getFederationMember(); ECKey ecKey = new ECKey(); ECKey.ECDSASignature ethSig = ecKey.doSign(new byte[]{}); @@ -334,7 +389,7 @@ void having_two_pegouts_signs_only_one() throws Exception { doReturn(ethSig).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -343,7 +398,6 @@ void having_two_pegouts_signs_only_one() throws Exception { mock(ReceiptStore.class) ); - BlockStore blockStore = mock(BlockStore.class); ReceiptStore receiptStore = mock(ReceiptStore.class); Keccak256 blockHash1 = createHash(2); @@ -393,7 +447,7 @@ void having_two_pegouts_signs_only_one() throws Exception { btcReleaseClient.start(federation); // Act - ethereumListener.get().onBestBlock(null, Collections.emptyList()); + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); // Assert verify(federatorSupport, times(1)).addSignature( @@ -405,6 +459,7 @@ void having_two_pegouts_signs_only_one() throws Exception { @Test void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Exception { Federation federation = TestUtils.createFederation(params, 9); + FederationMember federationMember = federation.getMembers().get(0); BtcTransaction pegout = TestUtils.createBtcTransaction(params, federation); Keccak256 pegoutCreationRskTxHash = createHash(0); SortedMap rskTxsWaitingForSignatures = new TreeMap<>(); @@ -420,6 +475,7 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex FederatorSupport federatorSupport = mock(FederatorSupport.class); doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + doReturn(federationMember).when(federatorSupport).getFederationMember(); ECKey ecKey = new ECKey(); BtcECKey fedKey = new BtcECKey(); @@ -431,7 +487,7 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -440,7 +496,6 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex mock(ReceiptStore.class) ); - BlockStore blockStore = mock(BlockStore.class); ReceiptStore receiptStore = mock(ReceiptStore.class); Keccak256 blockHash = createHash(2); @@ -487,7 +542,7 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); // Start first round of execution - ethereumListener.get().onBestBlock(null, Collections.emptyList()); + ethereumListener.get().onBestBlock(bestBlock, 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 @@ -495,7 +550,7 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); // Execute second round of execution - ethereumListener.get().onBestBlock(null, Collections.emptyList()); + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); // Verify we only send the add_signature tx to the bridge once // throughout both rounds of execution @@ -508,6 +563,7 @@ void onBestBlock_whenPegoutTxIsCached_shouldNotSignSamePegoutTxAgain() throws Ex @Test void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTxAgain() throws Exception { Federation federation = TestUtils.createFederation(params, 9); + FederationMember federationMember = federation.getMembers().get(0); BtcTransaction pegout = TestUtils.createBtcTransaction(params, federation); Keccak256 pegoutCreationRskTxHash = createHash(0); SortedMap rskTxsWaitingForSignatures = new TreeMap<>(); @@ -523,6 +579,7 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx FederatorSupport federatorSupport = mock(FederatorSupport.class); doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + doReturn(federationMember).when(federatorSupport).getFederationMember(); ECKey ecKey = new ECKey(); BtcECKey fedKey = new BtcECKey(); @@ -534,7 +591,7 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -543,7 +600,6 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx mock(ReceiptStore.class) ); - BlockStore blockStore = mock(BlockStore.class); ReceiptStore receiptStore = mock(ReceiptStore.class); Keccak256 blockHash = createHash(2); @@ -591,7 +647,7 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx btcReleaseClient.start(federation); // Start first round of execution - ethereumListener.get().onBestBlock(null, Collections.emptyList()); + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); // Ensure the pegout tx becomes invalid by advancing the clock 1 hour field = pegoutSignedCache.getClass().getDeclaredField("clock"); @@ -604,7 +660,7 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx () -> btcReleaseClient.validateTxIsNotCached(pegoutCreationRskTxHash)); // Execute second round of execution - ethereumListener.get().onBestBlock(null, Collections.emptyList()); + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); // Verify we send the add_signature tx to the bridge twice // throughout both rounds of execution @@ -614,10 +670,438 @@ void onBestBlock_whenPegoutTxIsCachedWithInvalidTimestamp_shouldSignSamePegoutTx ); } + @Test + void onBestBlock_whenOnlySvpSpendTxWaitingForSignaturesIsAvailable_shouldAddSignature() throws Exception { + // Arrange + Federation proposedFederation = TestUtils.createFederation(params, 9); + FederationMember federationMember = proposedFederation.getMembers().get(0); + BtcTransaction svpSpendTx = TestUtils.createBtcTransaction(params, proposedFederation); + Keccak256 svpSpendCreationRskTxHash = createHash(0); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendCreationRskTxHash, svpSpendTx); + StateForProposedFederator stateForProposedFederator = new StateForProposedFederator(svpSpendTxWFS); + + 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(federationMember).when(federatorSupport).getFederationMember(); + // return svp spend tx waiting for signatures + doReturn(Optional.of(stateForProposedFederator)).when(federatorSupport).getStateForProposedFederator(); + // returns zero pegouts waiting for signatures + doReturn(mock(StateForFederator.class)).when(federatorSupport).getStateForFederator(); + + ECKey ecKey = new ECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(federationMember.getBtcPublicKey().getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); + when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + Keccak256 blockHash = createHash(2); + Long blockNumber = 0L; + Block block = mock(Block.class); + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(block.getNumber()).thenReturn(blockNumber); + when(blockStore.getBlockByHash(blockHash.getBytes())).thenReturn(block); + when(txInfo.getReceipt()).thenReturn(txReceipt); + when(txInfo.getBlockHash()).thenReturn(blockHash.getBytes()); + when(receiptStore.getInMainChain(svpSpendCreationRskTxHash.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, + powpegNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + ActivationConfig activationConfig = mock(ActivationConfig.class); + when(activationConfig.isActive(ConsensusRule.RSKIP419, bestBlock.getNumber())).thenReturn(true); + + btcReleaseClient.setup( + signer, + activationConfig, + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(proposedFederation); + + // Act + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); + + // Assert + verify(federatorSupport).addSignature( + anyList(), + any(byte[].class) + ); + } + + @Test + void onBestBlock_whenBothPegoutAndSvpSpendTxWaitingForSignaturesAreAvailableAndFederatorIsOnlyPartOfProposedFederation_shouldOnlyAddOneSignature() throws Exception { + // Arrange + 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); + + Federation proposedFederation = TestUtils.createFederation(params, 9); + FederationMember federationMember = proposedFederation.getMembers().get(0); + BtcTransaction svpSpendTx = TestUtils.createBtcTransaction(params, proposedFederation); + Keccak256 svpSpendCreationRskTxHash = createHash(1); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendCreationRskTxHash, svpSpendTx); + StateForProposedFederator stateForProposedFederator = new StateForProposedFederator(svpSpendTxWFS); + + 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(federationMember).when(federatorSupport).getFederationMember(); + // returns pegout waiting for signatures + doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + // return svp spend tx waiting for signatures + doReturn(Optional.of(stateForProposedFederator)).when(federatorSupport).getStateForProposedFederator(); + + ECKey ecKey = new ECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(federationMember.getBtcPublicKey().getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); + when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + // pegout + Keccak256 blockHash = createHash(2); + Long blockNumber = 0L; + Block block = mock(Block.class); + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(block.getNumber()).thenReturn(blockNumber); + 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)); + + // svp spend tx + Keccak256 svpSpendBlockHash = createHash(3); + Long svpSpendBlockNumber = 1L; + Block svpSpendBlock = mock(Block.class); + TransactionReceipt svpSpendTxReceipt = mock(TransactionReceipt.class); + TransactionInfo svpSpendTxInfo = mock(TransactionInfo.class); + when(svpSpendBlock.getHash()).thenReturn(svpSpendBlockHash); + when(svpSpendBlock.getNumber()).thenReturn(svpSpendBlockNumber); + when(blockStore.getBlockByHash(svpSpendBlockHash.getBytes())).thenReturn(svpSpendBlock); + when(svpSpendTxInfo.getReceipt()).thenReturn(svpSpendTxReceipt); + when(svpSpendTxInfo.getBlockHash()).thenReturn(svpSpendBlockHash.getBytes()); + when(receiptStore.getInMainChain(svpSpendCreationRskTxHash.getBytes(), blockStore)).thenReturn(Optional.of(svpSpendTxInfo)); + + ReleaseCreationInformationGetter releaseCreationInformationGetter = + new ReleaseCreationInformationGetter( + receiptStore, blockStore + ); + + BtcReleaseClientStorageSynchronizer storageSynchronizer = + mock(BtcReleaseClientStorageSynchronizer.class); + when(storageSynchronizer.isSynced()).thenReturn(true); + + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( + ethereum, + federatorSupport, + powpegNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + ActivationConfig activationConfig = mock(ActivationConfig.class); + when(activationConfig.isActive(ConsensusRule.RSKIP419, bestBlock.getNumber())).thenReturn(true); + + btcReleaseClient.setup( + signer, + activationConfig, + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(proposedFederation); + + // Act + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); + + // Assert + verify(federatorSupport).addSignature( + anyList(), + any(byte[].class) + ); + } + + + @Test + void onBestBlock_whenBothPegoutAndSvpSpendTxWaitingForSignaturesIsAvailable_shouldAddSignatureForBoth() throws Exception { + // Arrange + List keys = Stream.generate(BtcECKey::new).limit(9).toList(); + BtcECKey fedKey = keys.get(0); + FederationMember federationMember = FederationMember.getFederationMembersFromKeys(keys).get(0); + Federation federation = TestUtils.createFederation(params, keys); + BtcTransaction pegout = TestUtils.createBtcTransaction(params, federation); + Keccak256 pegoutCreationRskTxHash = createHash(0); + SortedMap rskTxsWaitingForSignatures = new TreeMap<>(); + rskTxsWaitingForSignatures.put(pegoutCreationRskTxHash, pegout); + StateForFederator stateForFederator = new StateForFederator(rskTxsWaitingForSignatures); + + List proposedKeys = Stream.generate(BtcECKey::new).limit(8).collect(Collectors.toList()); + proposedKeys.add(fedKey); + Federation proposedFederation = TestUtils.createFederation(params, proposedKeys); + BtcTransaction svpSpendTx = TestUtils.createBtcTransaction(params, proposedFederation); + Keccak256 svpSpendCreationRskTxHash = createHash(1); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendCreationRskTxHash, svpSpendTx); + StateForProposedFederator stateForProposedFederator = new StateForProposedFederator(svpSpendTxWFS); + + 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(federationMember).when(federatorSupport).getFederationMember(); + // returns pegout waiting for signatures + doReturn(stateForFederator).when(federatorSupport).getStateForFederator(); + // return svp spend tx waiting for signatures + doReturn(Optional.of(stateForProposedFederator)).when(federatorSupport).getStateForProposedFederator(); + + ECKey ecKey = new ECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(fedKey.getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); + when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + // pegout + Keccak256 blockHash = createHash(2); + Long blockNumber = 0L; + Block block = mock(Block.class); + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(block.getNumber()).thenReturn(blockNumber); + 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)); + + // svp spend tx + Keccak256 svpSpendBlockHash = createHash(3); + Long svpSpendBlockNumber = 1L; + Block svpSpendBlock = mock(Block.class); + TransactionReceipt svpSpendTxReceipt = mock(TransactionReceipt.class); + TransactionInfo svpSpendTxInfo = mock(TransactionInfo.class); + when(svpSpendBlock.getHash()).thenReturn(svpSpendBlockHash); + when(svpSpendBlock.getNumber()).thenReturn(svpSpendBlockNumber); + when(blockStore.getBlockByHash(svpSpendBlockHash.getBytes())).thenReturn(svpSpendBlock); + when(svpSpendTxInfo.getReceipt()).thenReturn(svpSpendTxReceipt); + when(svpSpendTxInfo.getBlockHash()).thenReturn(svpSpendBlockHash.getBytes()); + when(receiptStore.getInMainChain(svpSpendCreationRskTxHash.getBytes(), blockStore)).thenReturn(Optional.of(svpSpendTxInfo)); + + ReleaseCreationInformationGetter releaseCreationInformationGetter = + new ReleaseCreationInformationGetter( + receiptStore, blockStore + ); + + BtcReleaseClientStorageSynchronizer storageSynchronizer = + mock(BtcReleaseClientStorageSynchronizer.class); + when(storageSynchronizer.isSynced()).thenReturn(true); + + BtcReleaseClient btcReleaseClient = new BtcReleaseClient( + ethereum, + federatorSupport, + powpegNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + ActivationConfig activationConfig = mock(ActivationConfig.class); + when(activationConfig.isActive(ConsensusRule.RSKIP419, bestBlock.getNumber())).thenReturn(true); + + btcReleaseClient.setup( + signer, + activationConfig, + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(federation); + btcReleaseClient.start(proposedFederation); + + // Act + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); + + // Assert + verify(federatorSupport, times(2)).addSignature( + anyList(), + any(byte[].class) + ); + } + + @Test + void onBestBlock_whenSvpSpendTxIsNotReadyToBeSigned_shouldNotAddSignature() throws Exception { + // Arrange + Federation proposedFederation = TestUtils.createFederation(params, 9); + FederationMember federationMember = proposedFederation.getMembers().get(0); + BtcTransaction svpSpendTx = TestUtils.createBtcTransaction(params, proposedFederation); + Keccak256 svpSpendCreationRskTxHash = createHash(0); + Map.Entry svpSpendTxWFS = new AbstractMap.SimpleEntry<>(svpSpendCreationRskTxHash, svpSpendTx); + StateForProposedFederator stateForProposedFederator = new StateForProposedFederator(svpSpendTxWFS); + + 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(federationMember).when(federatorSupport).getFederationMember(); + // return svp spend tx waiting for signatures + doReturn(Optional.of(stateForProposedFederator)).when(federatorSupport).getStateForProposedFederator(); + // returns zero pegouts waiting for signatures + doReturn(mock(StateForFederator.class)).when(federatorSupport).getStateForFederator(); + + ECKey ecKey = new ECKey(); + ECPublicKey signerPublicKey = new ECPublicKey(federationMember.getBtcPublicKey().getPubKey()); + + ECDSASigner signer = mock(ECDSASigner.class); + doReturn(signerPublicKey).when(signer).getPublicKey(BTC.getKeyId()); + doReturn(1).when(signer).getVersionForKeyId(ArgumentMatchers.any(KeyId.class)); + doReturn(ecKey.doSign(new byte[]{})).when(signer).sign(any(KeyId.class), any(SignerMessage.class)); + + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); + when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) + .thenReturn(PEGOUT_SIGNED_CACHE_TTL); + + SignerMessageBuilderFactory signerMessageBuilderFactory = new SignerMessageBuilderFactory( + mock(ReceiptStore.class) + ); + + // block is the best block, which will not pass the confirmation difference + Keccak256 blockHash = bestBlock.getHash(); + Long blockNumber = bestBlock.getNumber(); + Block block = bestBlock; + TransactionReceipt txReceipt = mock(TransactionReceipt.class); + TransactionInfo txInfo = mock(TransactionInfo.class); + when(block.getHash()).thenReturn(blockHash); + when(block.getNumber()).thenReturn(blockNumber); + when(blockStore.getBlockByHash(blockHash.getBytes())).thenReturn(block); + when(txInfo.getReceipt()).thenReturn(txReceipt); + when(txInfo.getBlockHash()).thenReturn(blockHash.getBytes()); + when(receiptStore.getInMainChain(svpSpendCreationRskTxHash.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, + powpegNodeSystemProperties, + mock(NodeBlockProcessor.class) + ); + + btcReleaseClient.setup( + signer, + mock(ActivationConfig.class), + signerMessageBuilderFactory, + releaseCreationInformationGetter, + mock(ReleaseRequirementsEnforcer.class), + mock(BtcReleaseClientStorageAccessor.class), + storageSynchronizer + ); + + btcReleaseClient.start(proposedFederation); + + // Act + ethereumListener.get().onBestBlock(bestBlock, Collections.emptyList()); + + // Assert + verify(federatorSupport, never()).addSignature( + anyList(), + any(byte[].class) + ); + } + + @Test void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); Ethereum ethereum = mock(Ethereum.class); AtomicReference ethereumListener = new AtomicReference<>(); @@ -628,9 +1112,10 @@ void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException }).when(ethereum).addListener(ArgumentMatchers.any(EthereumListener.class)); FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -656,7 +1141,7 @@ void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException btcReleaseClient.start(federation); // Act - ethereumListener.get().onBestBlock(null, null); + ethereumListener.get().onBestBlock(bestBlock, null); // Assert verify(federatorSupport, never()).getStateForFederator(); @@ -666,6 +1151,7 @@ void onBestBlock_return_when_node_is_syncing() throws BtcReleaseClientException void onBestBlock_return_when_pegout_is_disabled() throws BtcReleaseClientException { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); Ethereum ethereum = mock(Ethereum.class); AtomicReference ethereumListener = new AtomicReference<>(); @@ -676,9 +1162,10 @@ void onBestBlock_return_when_pegout_is_disabled() throws BtcReleaseClientExcepti }).when(ethereum).addListener(any(EthereumListener.class)); FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(false).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -704,7 +1191,7 @@ void onBestBlock_return_when_pegout_is_disabled() throws BtcReleaseClientExcepti btcReleaseClient.start(federation); // Act - ethereumListener.get().onBestBlock(null, null); + ethereumListener.get().onBestBlock(bestBlock, null); // Assert verify(federatorSupport, never()).getStateForFederator(); @@ -714,6 +1201,7 @@ void onBestBlock_return_when_pegout_is_disabled() throws BtcReleaseClientExcepti void onBlock_return_when_node_is_syncing() { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); Ethereum ethereum = mock(Ethereum.class); AtomicReference ethereumListener = new AtomicReference<>(); @@ -724,9 +1212,10 @@ void onBlock_return_when_node_is_syncing() { }).when(ethereum).addListener(any(EthereumListener.class)); FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(true).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -758,6 +1247,7 @@ void onBlock_return_when_node_is_syncing() { void onBlock_return_when_pegout_is_disabled() { // Arrange Federation federation = TestUtils.createFederation(params, 1); + FederationMember federationMember = federation.getMembers().get(0); Ethereum ethereum = mock(Ethereum.class); AtomicReference ethereumListener = new AtomicReference<>(); @@ -768,9 +1258,10 @@ void onBlock_return_when_pegout_is_disabled() { }).when(ethereum).addListener(any(EthereumListener.class)); FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - doReturn(Constants.regtest()).when(powpegNodeSystemProperties).getNetworkConstants(); + doReturn(Constants.mainnet()).when(powpegNodeSystemProperties).getNetworkConstants(); doReturn(false).when(powpegNodeSystemProperties).isPegoutEnabled(); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -862,6 +1353,7 @@ void validateTxCanBeSigned_federatorAlreadySigned() throws Exception { FederationArgs federationArgs = new FederationArgs(fedMembers, Instant.now(), 0, params); Federation federation = FederationFactory.buildStandardMultiSigFederation(federationArgs); + FederationMember federationMember = federation.getMembers().get(0); // Create a tx from the Fed to a random btc address BtcTransaction releaseTx = new BtcTransaction(params); @@ -884,20 +1376,24 @@ void validateTxCanBeSigned_federatorAlreadySigned() throws Exception { releaseInput.setScriptSig(inputScript); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); ECPublicKey signerPublicKey = new ECPublicKey(federator1PrivKey.getPubKey()); ECDSASigner signer = mock(ECDSASigner.class); Mockito.doReturn(signerPublicKey).when(signer).getPublicKey(ArgumentMatchers.any(KeyId.class)); + + FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); BtcReleaseClient client = new BtcReleaseClient( mock(Ethereum.class), - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); + client.setup( signer, mock(ActivationConfig.class), @@ -907,6 +1403,7 @@ void validateTxCanBeSigned_federatorAlreadySigned() throws Exception { mock(BtcReleaseClientStorageAccessor.class), mock(BtcReleaseClientStorageSynchronizer.class) ); + client.start(federation); // Act @@ -924,7 +1421,7 @@ void validateTxCanBeSigned_federationCantSign() throws Exception { releaseTx.addInput(releaseInput); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -987,7 +1484,7 @@ void removeSignaturesFromTransaction() { releaseInput.setScriptSig(inputScript); PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -1065,8 +1562,13 @@ private void test_validateTxCanBeSigned( BtcTransaction releaseTx, ECPublicKey signerPublicKey ) throws Exception { + FederationMember federationMember = federation.getMembers().get(0); + + FederatorSupport federatorSupport = mock(FederatorSupport.class); + doReturn(federationMember).when(federatorSupport).getFederationMember(); + PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -1075,10 +1577,11 @@ private void test_validateTxCanBeSigned( BtcReleaseClient client = new BtcReleaseClient( mock(Ethereum.class), - mock(FederatorSupport.class), + federatorSupport, powpegNodeSystemProperties, mock(NodeBlockProcessor.class) ); + client.setup( signer, mock(ActivationConfig.class), @@ -1088,6 +1591,7 @@ private void test_validateTxCanBeSigned( mock(BtcReleaseClientStorageAccessor.class), mock(BtcReleaseClientStorageSynchronizer.class) ); + client.start(federation); // Act @@ -1099,7 +1603,7 @@ private void test_extractStandardRedeemScript( Script redeemScriptToExtract) { PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -1172,7 +1676,7 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) HSMReleaseCreationInformationException, ReleaseRequirementsEnforcerException, HSMUnsupportedVersionException, SignerMessageBuilderException { PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.isPegoutEnabled()).thenReturn(true); when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); @@ -1185,6 +1689,7 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) BtcECKey key3 = new BtcECKey(); List keys = Arrays.asList(key1, key2, key3); Federation federation = createFederation(keys); + FederationMember federationMember = federation.getMembers().get(0); // Release info Keccak256 rskTxHash = createHash(0); @@ -1207,6 +1712,7 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) when(federatorSupport.getStateForFederator()).thenReturn( new StateForFederator(rskTxsWaitingForSignatures) // Only return the confirmed release ); + when(federatorSupport.getFederationMember()).thenReturn(federationMember); ECDSASigner signer = mock(ECDSASigner.class); when(signer.getVersionForKeyId(any())).thenReturn(2); @@ -1263,7 +1769,7 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) btcReleaseClient.start(federation); // Release "confirmed" - ethereumImpl.addBestBlockWithReceipts(mock(Block.class), new ArrayList<>()); + ethereumImpl.addBestBlockWithReceipts(bestBlock, new ArrayList<>()); // Verify the rsk tx hash was updated verify(releaseCreationInformationGetter, times(1)).getTxInfoToSign( @@ -1279,7 +1785,7 @@ private void testUsageOfStorageWhenSigning(boolean shouldHaveDataInFile) private BtcReleaseClient createBtcClient() { PowpegNodeSystemProperties powpegNodeSystemProperties = mock(PowpegNodeSystemProperties.class); - when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.regtest()); + when(powpegNodeSystemProperties.getNetworkConstants()).thenReturn(Constants.mainnet()); when(powpegNodeSystemProperties.isPegoutEnabled()).thenReturn(true); // Enabled by default when(powpegNodeSystemProperties.getPegoutSignedCacheTtl()) .thenReturn(PEGOUT_SIGNED_CACHE_TTL); diff --git a/src/test/java/co/rsk/federate/watcher/FederationWatcherListenerImplTest.java b/src/test/java/co/rsk/federate/watcher/FederationWatcherListenerImplTest.java new file mode 100644 index 000000000..b7cd365fe --- /dev/null +++ b/src/test/java/co/rsk/federate/watcher/FederationWatcherListenerImplTest.java @@ -0,0 +1,133 @@ +package co.rsk.federate.watcher; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import co.rsk.bitcoinj.core.BtcECKey; +import co.rsk.bitcoinj.core.NetworkParameters; +import co.rsk.federate.BtcToRskClient; +import co.rsk.federate.bitcoin.BitcoinWrapper; +import co.rsk.federate.btcreleaseclient.BtcReleaseClient; +import co.rsk.peg.federation.Federation; +import co.rsk.peg.federation.FederationArgs; +import co.rsk.peg.federation.FederationFactory; +import co.rsk.peg.federation.FederationMember; +import java.math.BigInteger; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.ethereum.crypto.ECKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FederationWatcherListenerImplTest { + + private static final NetworkParameters NETWORK_PARAMETERS = NetworkParameters.fromID(NetworkParameters.ID_REGTEST); + + private static final List FEDERATION_MEMBERS = + getFederationMembersFromPksForBtc(1000, 2000, 3000, 4000); + private static final long CREATION_BLOCK_NUMBER = 0L; + private static final Instant FEDERATION_CREATION_TIME = Instant.ofEpochMilli(5005L); + private static final FederationArgs FEDERATION_ARGS = new FederationArgs( + FEDERATION_MEMBERS, FEDERATION_CREATION_TIME, CREATION_BLOCK_NUMBER, NETWORK_PARAMETERS); + private static final Federation FEDERATION = FederationFactory.buildStandardMultiSigFederation(FEDERATION_ARGS); + + private BtcToRskClient btcToRskClientActive; + private BtcToRskClient btcToRskClientRetiring; + private BtcReleaseClient btcReleaseClient; + private BitcoinWrapper bitcoinWrapper; + private FederationWatcherListener federationWatcherListener; + + @BeforeEach + void setUp() { + btcToRskClientActive = mock(BtcToRskClient.class); + btcToRskClientRetiring = mock(BtcToRskClient.class); + btcReleaseClient = mock(BtcReleaseClient.class); + bitcoinWrapper = mock(BitcoinWrapper.class); + federationWatcherListener = new FederationWatcherListenerImpl( + btcToRskClientActive, btcToRskClientRetiring, btcReleaseClient, bitcoinWrapper); + } + + @Test + void onActiveFederationChange_whenFederationIsValid_shouldTriggerClientChange() { + // Act + federationWatcherListener.onActiveFederationChange(FEDERATION); + + // Assert + verify(btcToRskClientActive).stop(); + verify(btcReleaseClient).stop(FEDERATION); + verify(btcToRskClientActive).start(FEDERATION); + verify(btcReleaseClient).start(FEDERATION); + } + + @Test + void onRetiringFederationChange_whenFederationIsNull_shouldClearRetiringFederationClient() { + // Act + federationWatcherListener.onRetiringFederationChange(null); + + // Assert + verify(btcToRskClientRetiring).stop(); + } + + @Test + void onRetiringFederationChange_whenFederationIsValid_shouldTriggerClientChange() { + // Act + federationWatcherListener.onRetiringFederationChange(FEDERATION); + + // Assert + verify(btcToRskClientRetiring).stop(); + verify(btcReleaseClient).stop(FEDERATION); + verify(btcToRskClientRetiring).start(FEDERATION); + verify(btcReleaseClient).start(FEDERATION); + } + + @Test + void triggerClientChange_whenExceptionOccurs_shouldHandleException() { + // Arrange + // Simulate an exception in one of the called methods + doThrow(new RuntimeException("Simulated exception")).when(btcToRskClientActive).stop(); + + // Act & Assert + assertDoesNotThrow(() -> federationWatcherListener.onActiveFederationChange(FEDERATION)); + } + + @Test + void onProposedFederationChange_whenNewProposedFederationIsNull_shouldNotStartClient() { + // Act + federationWatcherListener.onProposedFederationChange(null); + + // Assert + verify(btcReleaseClient, never()).start(any(Federation.class)); + verify(bitcoinWrapper, never()).addFederationListener(any(Federation.class), any(BtcToRskClient.class)); + } + + @Test + void onProposedFederationChange_whenNewProposedFederationIsValid_shouldStartClient() { + // Act + federationWatcherListener.onProposedFederationChange(FEDERATION); + + // Assert + verify(btcReleaseClient).start(FEDERATION); + verify(bitcoinWrapper).addFederationListener(FEDERATION, btcToRskClientActive); + } + + @Test + void onProposedFederationChange_whenClientStartThrowsException_shouldHandleException() { + // Arrange + doThrow(new RuntimeException("Start failed")).when(btcReleaseClient).start(FEDERATION); + + // Act & Assert + assertDoesNotThrow(() -> federationWatcherListener.onProposedFederationChange(FEDERATION)); + } + + private static List getFederationMembersFromPksForBtc(Integer... pks) { + return Arrays.stream(pks).map(n -> new FederationMember( + BtcECKey.fromPrivate(BigInteger.valueOf(n)), + new ECKey(), + new ECKey())).toList(); + } +} diff --git a/src/test/java/co/rsk/federate/watcher/FederationWatcherTest.java b/src/test/java/co/rsk/federate/watcher/FederationWatcherTest.java new file mode 100644 index 000000000..28b0b02ae --- /dev/null +++ b/src/test/java/co/rsk/federate/watcher/FederationWatcherTest.java @@ -0,0 +1,417 @@ +package co.rsk.federate.watcher; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import co.rsk.bitcoinj.core.BtcECKey; +import co.rsk.bitcoinj.core.NetworkParameters; +import co.rsk.federate.FederationProvider; +import co.rsk.federate.signing.utils.TestUtils; +import co.rsk.peg.federation.Federation; +import co.rsk.peg.federation.FederationArgs; +import co.rsk.peg.federation.FederationFactory; +import co.rsk.peg.federation.FederationMember; +import java.math.BigInteger; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import org.ethereum.crypto.ECKey; +import org.ethereum.facade.Ethereum; +import org.ethereum.listener.EthereumListenerAdapter; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +class FederationWatcherTest { + + // Constants for network and block information + private static final NetworkParameters NETWORK_PARAMETERS = NetworkParameters.fromID(NetworkParameters.ID_REGTEST); + private static final long CREATION_BLOCK_NUMBER = 0L; + + // First federation constants + private static final List FIRST_FEDERATION_MEMBERS = + getFederationMembersFromPksForBtc(1000, 2000, 3000, 4000); + private static final Instant FIRST_FEDERATION_CREATION_TIME = Instant.ofEpochMilli(5005L); + private static final FederationArgs FIRST_FEDERATION_ARGS = new FederationArgs( + FIRST_FEDERATION_MEMBERS, FIRST_FEDERATION_CREATION_TIME, CREATION_BLOCK_NUMBER, NETWORK_PARAMETERS); + private static final Federation FIRST_FEDERATION = FederationFactory.buildStandardMultiSigFederation(FIRST_FEDERATION_ARGS); + + // Second federation constants + private static final List SECOND_FEDERATION_MEMBERS = + getFederationMembersFromPksForBtc(2000, 3000, 4000, 5000, 6000, 7000); + private static final Instant SECOND_FEDERATION_CREATION_TIME = Instant.ofEpochMilli(15300L); + private static final FederationArgs SECOND_FEDERATION_ARGS = new FederationArgs( + SECOND_FEDERATION_MEMBERS, SECOND_FEDERATION_CREATION_TIME, CREATION_BLOCK_NUMBER, NETWORK_PARAMETERS); + private static final Federation SECOND_FEDERATION = FederationFactory.buildStandardMultiSigFederation(SECOND_FEDERATION_ARGS); + + // Third federation constants + private static final List THIRD_FEDERATION_MEMBERS = + getFederationMembersFromPksForBtc(5000, 6000, 7000); + private static final Instant THIRD_FEDERATION_CREATION_TIME = Instant.ofEpochMilli(7400L); + private static final FederationArgs THIRD_FEDERATION_ARGS = new FederationArgs( + THIRD_FEDERATION_MEMBERS, THIRD_FEDERATION_CREATION_TIME, CREATION_BLOCK_NUMBER, NETWORK_PARAMETERS); + private static final Federation THIRD_FEDERATION = FederationFactory.buildStandardMultiSigFederation(THIRD_FEDERATION_ARGS); + + private final Ethereum rsk = mock(Ethereum.class); + private final FederationProvider federationProvider = mock(FederationProvider.class); + private final FederationWatcherListener federationWatcherListener = mock(FederationWatcherListener.class); + private final FederationWatcher federationWatcher = new FederationWatcher(rsk); + + @Test + void start_whenFederationWatcherIsSetUp_shouldAddListener() throws Exception { + // Act + federationWatcher.start(federationProvider, federationWatcherListener); + + // Assert + verify(rsk).addListener(any()); + assertSame(TestUtils.getInternalState(federationWatcher, "federationProvider"), federationProvider); + assertSame(TestUtils.getInternalState(federationWatcher, "federationWatcherListener"), federationWatcherListener); + } + + @Test + void onBestBlock_whenNoActiveFederationAndProposedFederationExists_shouldTriggerActiveAndProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, null, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.of(SECOND_FEDERATION.getAddress())); + when(federationProvider.getProposedFederation()).thenReturn(Optional.of(SECOND_FEDERATION)); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getActiveFederation()).thenReturn(FIRST_FEDERATION); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider).getProposedFederation(); + verify(federationWatcherListener).onProposedFederationChange(SECOND_FEDERATION); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider).getActiveFederation(); + verify(federationWatcherListener).onActiveFederationChange(FIRST_FEDERATION); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenProposedFederationChanged_shouldTriggerProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(SECOND_FEDERATION, FIRST_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.of(THIRD_FEDERATION.getAddress())); + when(federationProvider.getProposedFederation()).thenReturn(Optional.of(THIRD_FEDERATION)); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider).getProposedFederation(); + verify(federationWatcherListener).onProposedFederationChange(THIRD_FEDERATION); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenProposedFederationIsCleared_shouldTriggerProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(SECOND_FEDERATION, FIRST_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getProposedFederation()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getActiveFederation()).thenReturn(FIRST_FEDERATION); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider).getProposedFederation(); + verify(federationWatcherListener).onProposedFederationChange(null); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenProposedFederationChangesToActive_shouldTriggerActiveAndProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(SECOND_FEDERATION, FIRST_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getProposedFederation()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(SECOND_FEDERATION.getAddress()); + when(federationProvider.getActiveFederation()).thenReturn(SECOND_FEDERATION); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getRetiringFederation()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider).getProposedFederation(); + verify(federationWatcherListener).onProposedFederationChange(null); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider).getActiveFederation(); + verify(federationWatcherListener).onActiveFederationChange(SECOND_FEDERATION); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenNoActiveFederation_shouldTriggerActiveFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, null, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getActiveFederation()).thenReturn(FIRST_FEDERATION); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider).getRetiringFederationAddress(); + verify(federationWatcherListener).onActiveFederationChange(FIRST_FEDERATION); + + verify(federationProvider).getActiveFederation(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenActiveFederationChangesFromActiveToOtherActive_shouldTriggerActiveFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, FIRST_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(SECOND_FEDERATION.getAddress()); + when(federationProvider.getActiveFederation()).thenReturn(SECOND_FEDERATION); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider).getActiveFederation(); + verify(federationWatcherListener).onActiveFederationChange(SECOND_FEDERATION); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenNoActiveFederationChange_shouldNotTriggerActiveOrRetiringOrProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, FIRST_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenNoActiveAndRetiringAndProposedChangeInFederation_shouldNotTriggerActiveOrRetiringOrProposedFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(THIRD_FEDERATION, FIRST_FEDERATION, SECOND_FEDERATION); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.of(THIRD_FEDERATION.getAddress())); + when(federationProvider.getActiveFederationAddress()).thenReturn(FIRST_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(SECOND_FEDERATION.getAddress())); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider, never()).getRetiringFederation(); + verify(federationWatcherListener, never()).onRetiringFederationChange(any(Federation.class)); + } + + @Test + void onBestBlock_whenActiveFederationChangesToRetiring_shouldTriggerRetiringFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, SECOND_FEDERATION, null); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(SECOND_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(FIRST_FEDERATION.getAddress())); + when(federationProvider.getRetiringFederation()).thenReturn(Optional.of(FIRST_FEDERATION)); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider).getRetiringFederation(); + verify(federationWatcherListener).onRetiringFederationChange(FIRST_FEDERATION); + } + + @Test + void onBestBlock_whenRetiringFederationChangesToNone_shouldTriggerRetiringFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, SECOND_FEDERATION, FIRST_FEDERATION); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(SECOND_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getRetiringFederation()).thenReturn(Optional.empty()); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider).getRetiringFederation(); + verify(federationWatcherListener).onRetiringFederationChange(null); + } + + @Test + void onBestBlock_whenRetiringFederationChangesToOtherRetiring_shouldTriggerRetiringFederationChange() throws Exception { + // Arrange + var rskListener = setupAndGetRskListener(null, THIRD_FEDERATION, FIRST_FEDERATION); + when(federationProvider.getProposedFederationAddress()).thenReturn(Optional.empty()); + when(federationProvider.getActiveFederationAddress()).thenReturn(THIRD_FEDERATION.getAddress()); + when(federationProvider.getRetiringFederationAddress()).thenReturn(Optional.of(SECOND_FEDERATION.getAddress())); + when(federationProvider.getRetiringFederation()).thenReturn(Optional.of(SECOND_FEDERATION)); + + federationWatcher.start(federationProvider, federationWatcherListener); + + // Act + rskListener.onBestBlock(null, null); + + // Assert + verify(federationProvider).getProposedFederationAddress(); + verify(federationProvider, never()).getProposedFederation(); + verify(federationWatcherListener, never()).onProposedFederationChange(any(Federation.class)); + + verify(federationProvider).getActiveFederationAddress(); + verify(federationProvider, never()).getActiveFederation(); + verify(federationWatcherListener, never()).onActiveFederationChange(any(Federation.class)); + + verify(federationProvider).getRetiringFederationAddress(); + verify(federationProvider).getRetiringFederation(); + verify(federationWatcherListener).onRetiringFederationChange(SECOND_FEDERATION); + } + + private EthereumListenerAdapter setupAndGetRskListener( + Federation proposedFederation, Federation activeFederation, Federation retiringFederation) throws Exception { + // Mock the behavior of adding a listener + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer((InvocationOnMock m) -> { + listenerRef.set(m.getArgument(0)); + return null; + }).when(rsk).addListener(any()); + + federationWatcher.start(federationProvider, null); + + // Set up federationWatcher and internal states + TestUtils.setInternalState(federationWatcher, "proposedFederation", proposedFederation); + TestUtils.setInternalState(federationWatcher, "activeFederation", activeFederation); + TestUtils.setInternalState(federationWatcher, "retiringFederation", retiringFederation); + + // Retrieve and return the listener + EthereumListenerAdapter listener = listenerRef.get(); + assertNotNull(listener); + + return listener; + } + + private static List getFederationMembersFromPksForBtc(Integer... pks) { + return Arrays.stream(pks).map(n -> new FederationMember( + BtcECKey.fromPrivate(BigInteger.valueOf(n)), + new ECKey(), + new ECKey())).toList(); + } +}