diff --git a/rskj-core/src/main/java/co/rsk/peg/Bridge.java b/rskj-core/src/main/java/co/rsk/peg/Bridge.java index 3370a545b32..9d9663a4831 100644 --- a/rskj-core/src/main/java/co/rsk/peg/Bridge.java +++ b/rskj-core/src/main/java/co/rsk/peg/Bridge.java @@ -156,6 +156,17 @@ public class Bridge extends PrecompiledContracts.PrecompiledContract { // Returns the block number of the creation of the retiring federation public static final CallTransaction.Function GET_RETIRING_FEDERATION_CREATION_BLOCK_NUMBER = BridgeMethods.GET_RETIRING_FEDERATION_CREATION_BLOCK_NUMBER.getFunction(); + // Returns the proposed federation bitcoin address + public static final CallTransaction.Function GET_PROPOSED_FEDERATION_ADDRESS = BridgeMethods.GET_PROPOSED_FEDERATION_ADDRESS.getFunction(); + // Returns the number of federates in the proposed federation + public static final CallTransaction.Function GET_PROPOSED_FEDERATION_SIZE = BridgeMethods.GET_PROPOSED_FEDERATION_SIZE.getFunction(); + // Returns the public key of given type the federator at the specified index for the current proposed federation + public static final CallTransaction.Function GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE = BridgeMethods.GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE.getFunction(); + // Returns the creation time of the proposed federation + public static final CallTransaction.Function GET_PROPOSED_FEDERATION_CREATION_TIME = BridgeMethods.GET_PROPOSED_FEDERATION_CREATION_TIME.getFunction(); + // Returns the block number of the creation of the proposed federation + public static final CallTransaction.Function GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER = BridgeMethods.GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER.getFunction(); + // Creates a new pending federation and returns its id public static final CallTransaction.Function CREATE_FEDERATION = BridgeMethods.CREATE_FEDERATION.getFunction(); // Adds the given key to the current pending federation @@ -828,9 +839,15 @@ public byte[] getFederatorPublicKeyOfType(Object[] args) throws VMException { public Long getFederationCreationTime(Object[] args) { logger.trace("getFederationCreationTime"); + Instant activeFederationCreationTime = bridgeSupport.getActiveFederationCreationTime(); + + if (!activations.isActive(ConsensusRule.RSKIP419)) { + // Return the creation time in milliseconds from the epoch + return activeFederationCreationTime.toEpochMilli(); + } - // Return the creation time in milliseconds from the epoch - return bridgeSupport.getActiveFederationCreationTime().toEpochMilli(); + // Return the creation time in seconds from the epoch + return activeFederationCreationTime.getEpochSecond(); } public long getFederationCreationBlockNumber(Object[] args) { @@ -903,15 +920,20 @@ public byte[] getRetiringFederatorPublicKeyOfType(Object[] args) throws VMExcept public Long getRetiringFederationCreationTime(Object[] args) { logger.trace("getRetiringFederationCreationTime"); - Instant creationTime = bridgeSupport.getRetiringFederationCreationTime(); + Instant retiringFederationCreationTime = bridgeSupport.getRetiringFederationCreationTime(); - if (creationTime == null) { + if (retiringFederationCreationTime == null) { // -1 is returned when no retiring federation return -1L; } - // Return the creation time in milliseconds from the epoch - return creationTime.toEpochMilli(); + if (!activations.isActive(ConsensusRule.RSKIP419)) { + // Return the creation time in milliseconds from the epoch + return retiringFederationCreationTime.toEpochMilli(); + } + + // Return the creation time in seconds from the epoch + return retiringFederationCreationTime.getEpochSecond(); } public long getRetiringFederationCreationBlockNumber(Object[] args) { diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java b/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java index 229829fe962..b3f5c252eb9 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java @@ -120,6 +120,10 @@ private static BtcTransaction deserializeBtcTransactionFromRawTx( public static byte[] serializeRskTxWaitingForSignatures( Map.Entry rskTxWaitingForSignaturesEntry) { + if (rskTxWaitingForSignaturesEntry == null) { + return RLP.encodedEmptyList(); + } + byte[][] serializedRskTxWaitingForSignaturesEntry = serializeRskTxWaitingForSignaturesEntry(rskTxWaitingForSignaturesEntry); return RLP.encodeList(serializedRskTxWaitingForSignaturesEntry); @@ -154,7 +158,6 @@ private static byte[][] serializeRskTxWaitingForSignaturesEntry( public static Map.Entry deserializeRskTxWaitingForSignatures( byte[] data, NetworkParameters networkParameters) { - if (data == null || data.length == 0) { return null; } @@ -187,6 +190,9 @@ public static SortedMap deserializeRskTxsWaitingForSi private static Map.Entry deserializeRskTxWaitingForSignaturesEntry( RLPList rlpList, int index, NetworkParameters networkParameters) { + if (rlpList.size() == 0) { + return null; + } RLPElement rskTxHashRLPElement = rlpList.get(index * 2); byte[] rskTxHashData = rskTxHashRLPElement.getRLPData(); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java index 1f573fa4c4b..27c6a05b34a 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java @@ -31,6 +31,8 @@ import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.core.Repository; import org.ethereum.vm.DataWord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; /** @@ -40,6 +42,7 @@ * @author Oscar Guindzberg */ public class BridgeStorageProvider { + private static final Logger logger = LoggerFactory.getLogger(BridgeStorageProvider.class); // Dummy value to use when saving key only indexes private static final byte TRUE_VALUE = (byte) 1; @@ -611,6 +614,11 @@ public void setSvpFundTxHashUnsigned(Sha256Hash hash) { this.isSvpFundTxHashUnsignedSet = true; } + public void clearSvpFundTxHashUnsigned() { + logger.info("[clearSvpFundTxHashUnsigned] Clearing fund tx hash unsigned."); + setSvpFundTxHashUnsigned(null); + } + private void saveSvpFundTxHashUnsigned() { if (!activations.isActive(RSKIP419) || !isSvpFundTxHashUnsignedSet) { return; @@ -619,7 +627,8 @@ private void saveSvpFundTxHashUnsigned() { safeSaveToRepository( SVP_FUND_TX_HASH_UNSIGNED, svpFundTxHashUnsigned, - BridgeSerializationUtils::serializeSha256Hash); + BridgeSerializationUtils::serializeSha256Hash + ); } public void setSvpFundTxSigned(BtcTransaction svpFundTxSigned) { @@ -627,6 +636,11 @@ public void setSvpFundTxSigned(BtcTransaction svpFundTxSigned) { this.isSvpFundTxSignedSet = true; } + public void clearSvpFundTxSigned() { + logger.info("[clearSvpFundTxSigned] Clearing fund tx signed."); + setSvpFundTxSigned(null); + } + private void saveSvpFundTxSigned() { if (!activations.isActive(RSKIP419) || !isSvpFundTxSignedSet) { return; @@ -643,6 +657,11 @@ public void setSvpSpendTxHashUnsigned(Sha256Hash hash) { this.isSvpSpendTxHashUnsignedSet = true; } + public void clearSvpSpendTxHashUnsigned() { + logger.info("[clearSvpSpendTxHashUnsigned] Clearing spend tx hash unsigned."); + setSvpSpendTxHashUnsigned(null); + } + private void saveSvpSpendTxHashUnsigned() { if (!activations.isActive(RSKIP419) || !isSvpSpendTxHashUnsignedSet) { return; @@ -667,6 +686,11 @@ public void setSvpSpendTxWaitingForSignatures(Map.Entry utxos, boolean isFlyoverCompatible) { - return BridgeUtils.getFederationsSpendWallet(btcContext, getLiveFederations(), utxos, isFlyoverCompatible, provider); + return BridgeUtils.getFederationsSpendWallet( + btcContext, + federationSupport.getLiveFederations(), + utxos, + isFlyoverCompatible, + provider + ); } /** @@ -349,7 +355,12 @@ public Wallet getUTXOBasedWalletForLiveFederations(List utxos, boolean isF * */ public Wallet getNoSpendWalletForLiveFederations(boolean isFlyoverCompatible) { - return BridgeUtils.getFederationsNoSpendWallet(btcContext, getLiveFederations(), isFlyoverCompatible, provider); + return BridgeUtils.getFederationsNoSpendWallet( + btcContext, + federationSupport.getLiveFederations(), + isFlyoverCompatible, + provider + ); } /** @@ -393,33 +404,22 @@ public void registerBtcTransaction( throw new RegisterBtcTransactionException("Transaction already processed"); } + FederationContext federationContext = federationSupport.getFederationContext(); PegTxType pegTxType = PegUtils.getTransactionType( activations, provider, bridgeConstants, - getActiveFederation(), - getRetiringFederation(), - getLastRetiredFederationP2SHScript(), + federationContext, btcTx, height ); + logger.info("[registerBtcTransaction][btctx: {}] This is a {} transaction type", btcTx.getHash(), pegTxType); switch (pegTxType) { - case PEGIN: - logger.debug("[registerBtcTransaction] This is a peg-in tx {}", btcTx.getHash()); - processPegIn(btcTx, rskTxHash, height); - break; - case PEGOUT_OR_MIGRATION: - logger.debug("[registerBtcTransaction] This is a peg-out or migration tx {}", btcTx.getHash()); - processPegoutOrMigration(btcTx); - if (svpIsOngoing()) { - updateSvpFundTransactionValuesIfPossible(btcTx); - } - break; - default: - String message = String.format("This is not a peg-in, a peg-out nor a migration tx %s", btcTx.getHash()); - logger.warn("[registerBtcTransaction][rsk tx {}] {}", rskTxHash, message); - panicProcessor.panic("btclock", message); + case PEGIN -> registerPegIn(btcTx, rskTxHash, height); + case PEGOUT_OR_MIGRATION -> registerNewUtxos(btcTx); + case SVP_FUND_TX -> registerSvpFundTx(btcTx); + case SVP_SPEND_TX -> registerSvpSpendTx(btcTx); } } catch (RegisterBtcTransactionException e) { logger.warn( @@ -431,34 +431,34 @@ public void registerBtcTransaction( } } - private void updateSvpFundTransactionValuesIfPossible(BtcTransaction transaction) { - provider.getSvpFundTxHashUnsigned() - .filter(svpFundTxHashUnsigned -> isTheSvpFundTransaction(svpFundTxHashUnsigned, transaction)) - .ifPresent(isTheSvpFundTransaction -> updateSvpFundTransactionValues(transaction)); + private void registerSvpFundTx(BtcTransaction btcTx) throws IOException { + registerNewUtxos(btcTx); // Need to register the change UTXO + + // If the SVP validation period is over, SVP related values should be cleared in the next call to updateCollections + // In that case, the fundTx will be identified as a regular peg-out tx and processed via #registerPegoutOrMigration + // This covers the case when the fundTx is registered between the validation period end and the next call to updateCollections + if (isSvpOngoing()) { + updateSvpFundTransactionValues(btcTx); + } } - private boolean isTheSvpFundTransaction(Sha256Hash svpFundTransactionHashUnsigned, BtcTransaction transaction) { - Sha256Hash transactionHash = transaction.getHash(); + private void registerSvpSpendTx(BtcTransaction btcTx) throws IOException { + registerNewUtxos(btcTx); + provider.clearSvpSpendTxHashUnsigned(); - if (!transaction.hasWitness()) { - BtcTransaction transactionCopyWithoutSignatures = new BtcTransaction(networkParameters, transaction.bitcoinSerialize()); // this is needed to not remove signatures from the actual tx - BitcoinUtils.removeSignaturesFromTransactionWithP2shMultiSigInputs(transactionCopyWithoutSignatures); - transactionHash = transactionCopyWithoutSignatures.getHash(); - } - return transactionHash.equals(svpFundTransactionHashUnsigned); + logger.info("[registerSvpSpendTx] Going to commit the proposed federation."); + federationSupport.commitProposedFederation(); } private void updateSvpFundTransactionValues(BtcTransaction transaction) { - logger.debug( - "[updateSvpFundTransactionValues] Transaction {} is the svp fund transaction. Going to update its values.", transaction + logger.info( + "[updateSvpFundTransactionValues] Transaction {} (wtxid:{}) is the svp fund transaction. Going to update its values", + transaction.getHash(), + transaction.getHash(true) ); provider.setSvpFundTxSigned(transaction); - provider.setSvpFundTxHashUnsigned(null); - } - - private Script getLastRetiredFederationP2SHScript() { - return federationSupport.getLastRetiredFederationP2SHScript().orElse(null); + provider.clearSvpFundTxHashUnsigned(); } @VisibleForTesting @@ -466,15 +466,15 @@ BtcBlockStoreWithCache getBtcBlockStore() { return btcBlockStore; } - protected void processPegIn( + protected void registerPegIn( BtcTransaction btcTx, Keccak256 rskTxHash, int height ) throws IOException, RegisterBtcTransactionException { - final String METHOD_NAME = "processPegIn"; + final String METHOD_NAME = "registerPegIn"; if (!activations.isActive(ConsensusRule.RSKIP379)) { - legacyProcessPegin(btcTx, rskTxHash, height); + legacyRegisterPegin(btcTx, rskTxHash, height); logger.info( "[{}] BTC Tx {} processed in RSK transaction {} using legacy function", METHOD_NAME, @@ -485,7 +485,7 @@ protected void processPegIn( } Coin totalAmount = computeTotalAmountSent(btcTx); - logger.debug("[{}}] Total amount sent: {}", METHOD_NAME, totalAmount); + logger.debug("[{}] Total amount sent: {}", METHOD_NAME, totalAmount); PeginInformation peginInformation = new PeginInformation( btcLockSenderProvider, @@ -506,10 +506,9 @@ protected void processPegIn( if (peginProcessAction == PeginProcessAction.CAN_BE_REGISTERED) { logger.debug("[{}] Peg-in is valid, going to register", METHOD_NAME); executePegIn(btcTx, peginInformation, totalAmount); - markTxAsProcessed(btcTx); } else { Optional rejectedPeginReasonOptional = peginEvaluationResult.getRejectedPeginReason(); - if (!rejectedPeginReasonOptional.isPresent()) { + if (rejectedPeginReasonOptional.isEmpty()) { // This flow should never be reached. There should always be a rejected pegin reason. String message = "Invalid state. No rejected reason was returned from evaluatePegin method"; logger.error("[{}}] {}", METHOD_NAME, message); @@ -550,7 +549,7 @@ private void handleUnprocessableBtcTx( /** * Legacy version for processing peg-ins - * Use instead {@link co.rsk.peg.BridgeSupport#processPegIn} + * Use instead {@link co.rsk.peg.BridgeSupport#registerPegIn} * * @param btcTx Peg-in transaction to process * @param rskTxHash Hash of the RSK transaction where the prg-in is being processed @@ -558,7 +557,7 @@ private void handleUnprocessableBtcTx( * @deprecated */ @Deprecated - private void legacyProcessPegin( + private void legacyRegisterPegin( BtcTransaction btcTx, Keccak256 rskTxHash, int height @@ -588,12 +587,12 @@ private void legacyProcessPegin( btcTx.getHash(), e.getMessage() ); - logger.warn("[legacyProcessPegin] {}", message); + logger.warn("[legacyRegisterPegin] {}", message); throw new RegisterBtcTransactionException(message); } int protocolVersion = peginInformation.getProtocolVersion(); - logger.debug("[legacyProcessPegin] Protocol version: {}", protocolVersion); + logger.debug("[legacyRegisterPegin] Protocol version: {}", protocolVersion); switch (protocolVersion) { case 0: processPegInVersionLegacy(btcTx, rskTxHash, height, peginInformation, totalAmount); @@ -604,11 +603,9 @@ private void legacyProcessPegin( default: markTxAsProcessed(btcTx); String message = String.format("Invalid peg-in protocol version: %d", protocolVersion); - logger.warn("[legacyProcessPegin] {}", message); + logger.warn("[legacyRegisterPegin] {}", message); throw new RegisterBtcTransactionException(message); } - - markTxAsProcessed(btcTx); } private void processPegInVersionLegacy( @@ -644,6 +641,7 @@ private void processPegInVersionLegacy( } generateRejectionRelease(btcTx, senderBtcAddress, rskTxHash, totalAmount); + markTxAsProcessed(btcTx); } } @@ -668,10 +666,11 @@ private void processPegInVersion1( } refundTxSender(btcTx, rskTxHash, peginInformation, totalAmount); + markTxAsProcessed(btcTx); } } - private void executePegIn(BtcTransaction btcTx, PeginInformation peginInformation, Coin amount) { + private void executePegIn(BtcTransaction btcTx, PeginInformation peginInformation, Coin amount) throws IOException { RskAddress rskDestinationAddress = peginInformation.getRskDestinationAddress(); Address senderBtcAddress = peginInformation.getSenderBtcAddress(); TxSenderAddressType senderBtcAddressType = peginInformation.getSenderBtcAddressType(); @@ -696,7 +695,7 @@ private void executePegIn(BtcTransaction btcTx, PeginInformation peginInformatio } // Save UTXOs from the federation(s) only if we actually locked the funds - saveNewUTXOs(btcTx); + registerNewUtxos(btcTx); } private void refundTxSender( @@ -726,18 +725,13 @@ private void markTxAsProcessed(BtcTransaction btcTx) throws IOException { long rskHeight = rskExecutionBlock.getNumber(); provider.setHeightBtcTxhashAlreadyProcessed(btcTx.getHash(false), rskHeight); logger.debug( - "[markTxAsProcessed] Mark btc transaction {} as processed at height {}", + "[markTxAsProcessed] Mark btc transaction {} (wtxid: {}) as processed at height {}", btcTx.getHash(), + btcTx.getHash(true), rskHeight ); } - protected void processPegoutOrMigration(BtcTransaction btcTx) throws IOException { - markTxAsProcessed(btcTx); - saveNewUTXOs(btcTx); - logger.info("[processPegoutOrMigration] BTC Tx {} processed in RSK", btcTx.getHash(false)); - } - private boolean shouldProcessPegInVersionLegacy( TxSenderAddressType txSenderAddressType, BtcTransaction btcTx, @@ -795,9 +789,11 @@ private void transferTo(RskAddress receiver, co.rsk.core.Coin amount) { } /* - Add the btcTx outputs that send btc to the federation(s) to the UTXO list + Add the btcTx outputs that send btc to the federation(s) to the UTXO list, + so they can be used as inputs in future peg-out transactions. + Finally, mark the btcTx as processed. */ - private void saveNewUTXOs(BtcTransaction btcTx) { + private void registerNewUtxos(BtcTransaction btcTx) throws IOException { // Outputs to the active federation Wallet activeFederationWallet = getActiveFederationWallet(false); List outputsToTheActiveFederation = btcTx.getWalletOutputs( @@ -814,7 +810,7 @@ private void saveNewUTXOs(BtcTransaction btcTx) { ); federationSupport.getActiveFederationBtcUTXOs().add(utxo); } - logger.debug("[saveNewUTXOs] Registered {} UTXOs sent to the active federation", outputsToTheActiveFederation.size()); + logger.debug("[registerNewUtxos] Registered {} UTXOs sent to the active federation", outputsToTheActiveFederation.size()); // Outputs to the retiring federation (if any) Wallet retiringFederationWallet = getRetiringFederationWallet(false); @@ -831,8 +827,11 @@ private void saveNewUTXOs(BtcTransaction btcTx) { ); federationSupport.getRetiringFederationBtcUTXOs().add(utxo); } - logger.debug("[saveNewUTXOs] Registered {} UTXOs sent to the retiring federation", outputsToTheRetiringFederation.size()); + logger.debug("[registerNewUtxos] Registered {} UTXOs sent to the retiring federation", outputsToTheRetiringFederation.size()); } + + markTxAsProcessed(btcTx); + logger.info("[registerNewUtxos] BTC Tx {} (wtxid: {}) processed in RSK", btcTx.getHash(), btcTx.getHash(true)); } /** @@ -1012,6 +1011,8 @@ public void updateCollections(Transaction rskTx) throws IOException { processConfirmedPegouts(rskTx); updateFederationCreationBlockHeights(); + + updateSvpState(rskTx); } private void logUpdateCollections(Transaction rskTx) { @@ -1019,48 +1020,108 @@ private void logUpdateCollections(Transaction rskTx) { eventLogger.logUpdateCollections(sender); } - private boolean svpIsOngoing() { - return federationSupport.getProposedFederation() - .filter(this::validationPeriodIsOngoing) - .isPresent(); + private void updateSvpState(Transaction rskTx) { + Optional proposedFederationOpt = federationSupport.getProposedFederation(); + if (proposedFederationOpt.isEmpty()) { + return; + } + + // if the proposed federation exists and the validation period ended, + // we can conclude that the svp failed + Federation proposedFederation = proposedFederationOpt.get(); + if (!isSvpOngoing()) { + processSvpFailure(proposedFederation); + return; + } + + Keccak256 rskTxHash = rskTx.getHash(); + + if (shouldCreateAndProcessSvpFundTransaction()) { + logger.info("[updateSvpState] No svp values were found, so fund tx creation will be processed."); + processSvpFundTransactionUnsigned(rskTxHash, proposedFederation); + } + + // if the fund tx signed is present, then the fund transaction change was registered, + // meaning we can create the spend tx. + Optional svpFundTxSigned = provider.getSvpFundTxSigned(); + if (svpFundTxSigned.isPresent()) { + logger.info( + "[updateSvpState] Fund tx signed was found, so spend tx creation will be processed." + ); + processSvpSpendTransactionUnsigned(rskTxHash, proposedFederation, svpFundTxSigned.get()); + } } - private boolean validationPeriodIsOngoing(Federation proposedFederation) { - long validationPeriodEndBlock = proposedFederation.getCreationBlockNumber() + - bridgeConstants.getFederationConstants().getValidationPeriodDurationInBlocks(); + private boolean shouldCreateAndProcessSvpFundTransaction() { + // the fund tx will be created when the svp starts, + // so we must ensure all svp values are clear to proceed with its creation + Optional svpFundTxHashUnsigned = provider.getSvpFundTxHashUnsigned(); + Optional svpFundTxSigned = provider.getSvpFundTxSigned(); + Optional svpSpendTxHashUnsigned = provider.getSvpSpendTxHashUnsigned(); // spendTxHash will be removed the last, after spendTxWFS, so is enough checking just this value - return rskExecutionBlock.getNumber() < validationPeriodEndBlock; + return svpFundTxHashUnsigned.isEmpty() + && svpFundTxSigned.isEmpty() + && svpSpendTxHashUnsigned.isEmpty(); } - protected void processSvpFundTransactionUnsigned(Transaction rskTx) throws IOException, InsufficientMoneyException { - Optional proposedFederationOpt = federationSupport.getProposedFederation(); - if (proposedFederationOpt.isEmpty()) { - String message = "Proposed federation should be present when processing SVP fund transaction."; - logger.warn(message); - throw new IllegalStateException(message); - } - Federation proposedFederation = proposedFederationOpt.get(); + private void processSvpFailure(Federation proposedFederation) { + logger.info( + "[processSvpFailure] Proposed federation validation failed at block {}. SVP failure will be processed and Federation election will be allowed again.", + rskExecutionBlock.getNumber() + ); + eventLogger.logCommitFederationFailure(rskExecutionBlock, proposedFederation); + allowFederationElectionAgain(); + } - Coin spendableValueFromProposedFederation = bridgeConstants.getSpendableValueFromProposedFederation(); - BtcTransaction svpFundTransactionUnsigned = createSvpFundTransaction(proposedFederation, spendableValueFromProposedFederation); + private void allowFederationElectionAgain() { + federationSupport.clearProposedFederation(); + provider.clearSvpValues(); + } - provider.setSvpFundTxHashUnsigned(svpFundTransactionUnsigned.getHash()); - PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations(); - settleReleaseRequest(pegoutsWaitingForConfirmations, svpFundTransactionUnsigned, rskTx.getHash(), spendableValueFromProposedFederation); + private boolean isSvpOngoing() { + return federationSupport + .getProposedFederation() + .map(proposedFederation -> rskExecutionBlock.getNumber() < proposedFederation.getCreationBlockNumber() + + bridgeConstants.getFederationConstants().getValidationPeriodDurationInBlocks() + ) + .orElse(false); } - private BtcTransaction createSvpFundTransaction(Federation proposedFederation, Coin spendableValueFromProposedFederation) throws InsufficientMoneyException { + private void processSvpFundTransactionUnsigned(Keccak256 rskTxHash, Federation proposedFederation) { + try { + BtcTransaction svpFundTransactionUnsigned = createSvpFundTransaction(proposedFederation); + provider.setSvpFundTxHashUnsigned(svpFundTransactionUnsigned.getHash()); + PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations(); + + List utxosToUse = federationSupport.getActiveFederationBtcUTXOs(); + // one output to proposed fed, one output to flyover proposed fed + Coin totalValueSentToProposedFederation = bridgeConstants.getSvpFundTxOutputsValue().multiply(2); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, svpFundTransactionUnsigned, rskTxHash, totalValueSentToProposedFederation); + } catch (InsufficientMoneyException e) { + logger.error( + "[processSvpFundTransactionUnsigned] Insufficient funds for creating the fund transaction. Error message: {}", + e.getMessage() + ); + } catch (IOException e) { + logger.error( + "[processSvpFundTransactionUnsigned] IOException getting the pegouts waiting for confirmations. Error message: {}", + e.getMessage() + ); + } + } + + private BtcTransaction createSvpFundTransaction(Federation proposedFederation) throws InsufficientMoneyException { Wallet activeFederationWallet = getActiveFederationWallet(true); BtcTransaction svpFundTransaction = new BtcTransaction(networkParameters); svpFundTransaction.setVersion(BTC_TX_VERSION_2); + Coin svpFundTxOutputsValue = bridgeConstants.getSvpFundTxOutputsValue(); // add outputs to proposed fed and proposed fed with flyover prefix - svpFundTransaction.addOutput(spendableValueFromProposedFederation, proposedFederation.getAddress()); - + svpFundTransaction.addOutput(svpFundTxOutputsValue, proposedFederation.getAddress()); Address proposedFederationWithFlyoverPrefixAddress = getFlyoverAddress(networkParameters, bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederation.getRedeemScript()); - svpFundTransaction.addOutput(spendableValueFromProposedFederation, proposedFederationWithFlyoverPrefixAddress); + svpFundTransaction.addOutput(svpFundTxOutputsValue, proposedFederationWithFlyoverPrefixAddress); // complete tx with input and change output SendRequest sendRequest = createSvpFundTransactionSendRequest(svpFundTransaction); @@ -1075,57 +1136,65 @@ private SendRequest createSvpFundTransactionSendRequest(BtcTransaction transacti sendRequest.feePerKb = feePerKbSupport.getFeePerKb(); sendRequest.missingSigsMode = Wallet.MissingSigsMode.USE_OP_ZERO; sendRequest.recipientsPayFees = false; + sendRequest.shuffleOutputs = false; return sendRequest; } - protected void processSvpSpendTransactionUnsigned(Transaction rskTx) { - federationSupport.getProposedFederation() - .ifPresent(proposedFederation -> provider.getSvpFundTxSigned() - .ifPresent(svpFundTxSigned -> { - BtcTransaction svpSpendTransactionUnsigned = createSvpSpendTransaction(svpFundTxSigned, proposedFederation); - - Keccak256 rskTxHash = rskTx.getHash(); - updateSvpSpendTransactionValues(rskTxHash, svpSpendTransactionUnsigned); + private void processSvpSpendTransactionUnsigned(Keccak256 rskTxHash, Federation proposedFederation, BtcTransaction svpFundTxSigned) { + BtcTransaction svpSpendTransactionUnsigned; + try { + svpSpendTransactionUnsigned = createSvpSpendTransaction(svpFundTxSigned, proposedFederation); + } catch (IllegalStateException e){ + logger.error("[processSvpSpendTransactionUnsigned] Error creating spend transaction {}", e.getMessage()); + return; + } + updateSvpSpendTransactionValues(rskTxHash, svpSpendTransactionUnsigned); - Coin amountSentToActiveFed = svpSpendTransactionUnsigned.getOutput(0).getValue(); - logReleaseRequested(rskTxHash, svpSpendTransactionUnsigned, amountSentToActiveFed); - logPegoutTransactionCreated(svpSpendTransactionUnsigned); - })); + Coin amountSentToActiveFed = svpSpendTransactionUnsigned.getOutput(0).getValue(); + logReleaseRequested(rskTxHash, svpSpendTransactionUnsigned, amountSentToActiveFed); + logPegoutTransactionCreated(svpSpendTransactionUnsigned); } - private BtcTransaction createSvpSpendTransaction(BtcTransaction svpFundTxSigned, Federation proposedFederation) { + private BtcTransaction createSvpSpendTransaction(BtcTransaction svpFundTxSigned, Federation proposedFederation) throws IllegalStateException { BtcTransaction svpSpendTransaction = new BtcTransaction(networkParameters); svpSpendTransaction.setVersion(BTC_TX_VERSION_2); - addSvpSpendTransactionInputs(svpSpendTransaction, svpFundTxSigned, proposedFederation); + Script proposedFederationRedeemScript = proposedFederation.getRedeemScript(); + TransactionOutput outputToProposedFed = searchForOutput( + svpFundTxSigned.getOutputs(), + proposedFederation.getP2SHScript() + ).orElseThrow(() -> new IllegalStateException("[createSvpSpendTransaction] Output to proposed federation was not found in fund transaction.")); + svpSpendTransaction.addInput(outputToProposedFed); + svpSpendTransaction.getInput(0).setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(proposedFederationRedeemScript)); + + Script flyoverRedeemScript = getFlyoverRedeemScript(bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederationRedeemScript); + Script flyoverOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); + TransactionOutput outputToFlyoverProposedFed = searchForOutput( + svpFundTxSigned.getOutputs(), + flyoverOutputScript + ).orElseThrow(() -> new IllegalStateException("[createSvpSpendTransaction] Output to flyover proposed federation was not found in fund transaction.")); + svpSpendTransaction.addInput(outputToFlyoverProposedFed); + svpSpendTransaction.getInput(1).setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(flyoverRedeemScript)); + + Coin valueSentToProposedFed = outputToProposedFed.getValue(); + Coin valueSentToFlyoverProposedFed = outputToFlyoverProposedFed.getValue(); + + Coin valueToSend = valueSentToProposedFed + .plus(valueSentToFlyoverProposedFed) + .minus(calculateSvpSpendTxFees(proposedFederation)); svpSpendTransaction.addOutput( - calculateSvpSpendTxAmount(proposedFederation), + valueToSend, federationSupport.getActiveFederationAddress() ); return svpSpendTransaction; } - private void addSvpSpendTransactionInputs(BtcTransaction svpSpendTransaction, BtcTransaction svpFundTxSigned, Federation proposedFederation) { - Script proposedFederationRedeemScript = proposedFederation.getRedeemScript(); - Script proposedFederationOutputScript = proposedFederation.getP2SHScript(); - addInputFromMatchingOutputScript(svpSpendTransaction, svpFundTxSigned, proposedFederationOutputScript); - svpSpendTransaction.getInput(0) - .setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(proposedFederationRedeemScript)); - - Script flyoverRedeemScript = - getFlyoverRedeemScript(bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederationRedeemScript); - Script flyoverOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); - addInputFromMatchingOutputScript(svpSpendTransaction, svpFundTxSigned, flyoverOutputScript); - svpSpendTransaction.getInput(1) - .setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(flyoverRedeemScript)); - } - - private Coin calculateSvpSpendTxAmount(Federation proposedFederation) { + private Coin calculateSvpSpendTxFees(Federation proposedFederation) { int svpSpendTransactionSize = calculatePegoutTxSize(activations, proposedFederation, 2, 1); - long svpSpendTransactionBackedUpSize = svpSpendTransactionSize * 12L / 10L; // just to be sure the amount sent will be enough + long svpSpendTransactionBackedUpSize = svpSpendTransactionSize * 12L / 10L; // just to be sure the fees sent will be enough return feePerKbSupport.getFeePerKb() .multiply(svpSpendTransactionBackedUpSize) @@ -1239,7 +1308,7 @@ private void migrateFunds( Keccak256 rskTxHash, Wallet retiringFederationWallet, Address activeFederationAddress, - List availableUTXOs) throws IOException { + List utxosToUse) throws IOException { PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations = provider.getPegoutsWaitingForConfirmations(); Pair> createResult = createMigrationTransaction(retiringFederationWallet, activeFederationAddress); @@ -1254,12 +1323,8 @@ private void migrateFunds( Coin amountMigrated = selectedUTXOs.stream() .map(UTXO::getValue) .reduce(Coin.ZERO, Coin::add); - settleReleaseRequest(pegoutsWaitingForConfirmations, migrationTransaction, rskTxHash, amountMigrated); - // Mark UTXOs as spent - availableUTXOs.removeIf(utxo -> selectedUTXOs.stream().anyMatch(selectedUtxo -> - utxo.getHash().equals(selectedUtxo.getHash()) && utxo.getIndex() == selectedUtxo.getIndex() - )); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, migrationTransaction, rskTxHash, amountMigrated); } /** @@ -1305,11 +1370,23 @@ private void processPegoutRequests(Transaction rskTx) { } } - private void settleReleaseRequest(PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction pegoutTransaction, Keccak256 releaseCreationTxHash, Coin requestedAmount) { - addPegoutToPegoutsWaitingForConfirmations(pegoutsWaitingForConfirmations, pegoutTransaction, releaseCreationTxHash); - savePegoutTxSigHash(pegoutTransaction); - logReleaseRequested(releaseCreationTxHash, pegoutTransaction, requestedAmount); - logPegoutTransactionCreated(pegoutTransaction); + private void settleReleaseRequest(List utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction releaseTransaction, Keccak256 releaseCreationTxHash, Coin requestedAmount) { + removeSpentUtxos(utxosToUse, releaseTransaction); + addPegoutToPegoutsWaitingForConfirmations(pegoutsWaitingForConfirmations, releaseTransaction, releaseCreationTxHash); + savePegoutTxSigHash(releaseTransaction); + logReleaseRequested(releaseCreationTxHash, releaseTransaction, requestedAmount); + logPegoutTransactionCreated(releaseTransaction); + } + + private void removeSpentUtxos(List utxosToUse, BtcTransaction releaseTx) { + List utxosToRemove = utxosToUse.stream() + .filter(utxo -> releaseTx.getInputs().stream().anyMatch(input -> + input.getOutpoint().getHash().equals(utxo.getHash()) && input.getOutpoint().getIndex() == utxo.getIndex()) + ).toList(); + + logger.debug("[removeSpentUtxos] Used {} UTXOs for this release", utxosToRemove.size()); + + utxosToUse.removeAll(utxosToRemove); } private void addPegoutToPegoutsWaitingForConfirmations(PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, BtcTransaction pegoutTransaction, Keccak256 releaseCreationTxHash) { @@ -1367,7 +1444,7 @@ private void logPegoutTransactionCreated(BtcTransaction pegoutTransaction) { private void processPegoutsIndividually( ReleaseRequestQueue pegoutRequests, ReleaseTransactionBuilder txBuilder, - List availableUTXOs, + List utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, Wallet wallet ) { @@ -1392,11 +1469,7 @@ private void processPegoutsIndividually( BtcTransaction generatedTransaction = result.getBtcTx(); Keccak256 pegoutCreationTxHash = pegoutRequest.getRskTxHash(); - settleReleaseRequest(pegoutsWaitingForConfirmations, generatedTransaction, pegoutCreationTxHash, pegoutRequest.getAmount()); - - // Mark UTXOs as spent - List selectedUTXOs = result.getSelectedUTXOs(); - availableUTXOs.removeAll(selectedUTXOs); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, generatedTransaction, pegoutCreationTxHash, pegoutRequest.getAmount()); adjustBalancesIfChangeOutputWasDust(generatedTransaction, pegoutRequest.getAmount(), wallet); @@ -1407,7 +1480,7 @@ private void processPegoutsIndividually( private void processPegoutsInBatch( ReleaseRequestQueue pegoutRequests, ReleaseTransactionBuilder txBuilder, - List availableUTXOs, + List utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, Wallet wallet, Transaction rskTx) { @@ -1456,18 +1529,13 @@ private void processPegoutsInBatch( BtcTransaction batchPegoutTransaction = result.getBtcTx(); Keccak256 batchPegoutCreationTxHash = rskTx.getHash(); - settleReleaseRequest(pegoutsWaitingForConfirmations, batchPegoutTransaction, batchPegoutCreationTxHash, totalPegoutValue); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, batchPegoutTransaction, batchPegoutCreationTxHash, totalPegoutValue); // Remove batched requests from the queue after successfully batching pegouts pegoutRequests.removeEntries(pegoutEntries); - // Mark UTXOs as spent - List selectedUTXOs = result.getSelectedUTXOs(); - logger.debug("[processPegoutsInBatch] used {} UTXOs for this pegout", selectedUTXOs.size()); - availableUTXOs.removeAll(selectedUTXOs); - eventLogger.logBatchPegoutCreated(batchPegoutTransaction.getHash(), - pegoutEntries.stream().map(ReleaseRequestQueue.Entry::getRskTxHash).collect(Collectors.toList())); + pegoutEntries.stream().map(ReleaseRequestQueue.Entry::getRskTxHash).toList()); adjustBalancesIfChangeOutputWasDust(batchPegoutTransaction, totalPegoutValue, wallet); } @@ -1605,13 +1673,13 @@ public void addSignature(BtcECKey federatorBtcPublicKey, List signatures Context.propagate(btcContext); - if (svpIsOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) { - logger.trace("[addSignature] Going to sign svp spend transaction with federator public key {}", federatorBtcPublicKey); + if (isSvpOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) { + logger.info("[addSignature] Going to sign svp spend transaction with federator public key {}", federatorBtcPublicKey); addSvpSpendTxSignatures(federatorBtcPublicKey, signatures); return; } - logger.trace("[addSignature] Going to sign release transaction with federator public key {}", federatorBtcPublicKey); + logger.info("[addSignature] Going to sign release transaction with federator public key {}", federatorBtcPublicKey); addReleaseSignatures(federatorBtcPublicKey, signatures, releaseCreationRskTxHash); } @@ -1694,30 +1762,32 @@ private void addSvpSpendTxSignatures( Federation proposedFederation = federationSupport.getProposedFederation() // This flow should never be reached. There should always be a proposed federation if svpIsOngoing. .orElseThrow(() -> new IllegalStateException("Proposed federation must exist when trying to sign the svp spend transaction.")); + Map.Entry svpSpendTxWFS = provider.getSvpSpendTxWaitingForSignatures() + // The svpSpendTxWFS should always be present at this point, since we already checked isTheSvpSpendTx. + .orElseThrow(() -> new IllegalStateException("Svp spend tx waiting for signatures must exist")); FederationMember federationMember = proposedFederation.getMemberByBtcPublicKey(proposedFederatorPublicKey) .orElseThrow(() -> new IllegalStateException("Federator must belong to proposed federation to sign the svp spend transaction.")); - provider.getSvpSpendTxWaitingForSignatures() - // The svpSpendTxWFS should always be present at this point, since we already checked isTheSvpSpendTx. - .ifPresent(svpSpendTxWFS -> { - - Keccak256 svpSpendTxCreationRskTxHash = svpSpendTxWFS.getKey(); - BtcTransaction svpSpendTx = svpSpendTxWFS.getValue(); + Keccak256 svpSpendTxCreationRskTxHash = svpSpendTxWFS.getKey(); + BtcTransaction svpSpendTx = svpSpendTxWFS.getValue(); - if (!areSignaturesEnoughToSignAllTxInputs(svpSpendTx, signatures)) { - return; - } + if (!areSignaturesEnoughToSignAllTxInputs(svpSpendTx, signatures)) { + return; + } - processSigning(federationMember, signatures, svpSpendTxCreationRskTxHash, svpSpendTx); + processSigning(federationMember, signatures, svpSpendTxCreationRskTxHash, svpSpendTx); + + // save current fed signature back in storage + svpSpendTxWFS.setValue(svpSpendTx); + provider.setSvpSpendTxWaitingForSignatures(svpSpendTxWFS); - if (!BridgeUtils.hasEnoughSignatures(btcContext, svpSpendTx)) { - logMissingSignatures(svpSpendTx, svpSpendTxCreationRskTxHash, proposedFederation); - return; - } + if (!BridgeUtils.hasEnoughSignatures(btcContext, svpSpendTx)) { + logMissingSignatures(svpSpendTx, svpSpendTxCreationRskTxHash, proposedFederation); + return; + } - logReleaseBtc(svpSpendTx, svpSpendTxCreationRskTxHash.getBytes()); - provider.setSvpSpendTxWaitingForSignatures(null); - }); + logReleaseBtc(svpSpendTx, svpSpendTxCreationRskTxHash.getBytes()); + provider.clearSvpSpendTxWaitingForSignatures(); } private boolean areSignaturesEnoughToSignAllTxInputs(BtcTransaction releaseTx, List signatures) { @@ -1879,19 +1949,20 @@ public byte[] getStateForBtcReleaseClient() throws IOException { /** * Retrieves the current SVP spend transaction state for the SVP client. + * *

* This method checks if there is an SVP spend transaction waiting for signatures, and if so, it serializes * the state into RLP format. If no transaction is waiting, it returns an encoded empty RLP list. *

* * @return A byte array representing the RLP-encoded state of the SVP spend transaction. If no transaction - * is waiting, returns an RLP-encoded empty list. + * is waiting, returns a double RLP-encoded empty list. */ public byte[] getStateForSvpClient() { return provider.getSvpSpendTxWaitingForSignatures() .map(StateForProposedFederator::new) .map(StateForProposedFederator::encodeToRlp) - .orElse(RLP.encodedEmptyList()); + .orElse(RLP.encodeList(RLP.encodedEmptyList())); } /** @@ -2206,186 +2277,84 @@ public Federation getActiveFederation() { return federationSupport.getActiveFederation(); } - /** - * Returns the currently retiring federation. - * See getRetiringFederationReference() for details. - * @return the retiring federation. - */ + @Nullable public Federation getRetiringFederation() { return federationSupport.getRetiringFederation(); } - /** - * Returns the active federation bitcoin address. - * @return the active federation bitcoin address. - */ public Address getActiveFederationAddress() { return federationSupport.getActiveFederationAddress(); } - /** - * Returns the active federation's size - * @return the active federation size - */ public Integer getActiveFederationSize() { return federationSupport.getActiveFederationSize(); } - /** - * Returns the active federation's minimum required signatures - * @return the active federation minimum required signatures - */ public Integer getActiveFederationThreshold() { return federationSupport.getActiveFederationThreshold(); } - /** - * Returns the public key of the active federation's federator at the given index - * @param index the federator's index (zero-based) - * @return the federator's public key - */ public byte[] getActiveFederatorBtcPublicKey(int index) { return federationSupport.getActiveFederatorBtcPublicKey(index); } - /** - * Returns the public key of given type of the active federation's federator at the given index - * @param index the federator's index (zero-based) - * @param keyType the key type - * @return the federator's public key - */ public byte[] getActiveFederatorPublicKeyOfType(int index, FederationMember.KeyType keyType) { return federationSupport.getActiveFederatorPublicKeyOfType(index, keyType); } - /** - * Returns the active federation's creation time - * @return the active federation creation time - */ public Instant getActiveFederationCreationTime() { return federationSupport.getActiveFederationCreationTime(); } - /** - * Returns the active federation's creation block number - * @return the active federation creation block number - */ public long getActiveFederationCreationBlockNumber() { return federationSupport.getActiveFederationCreationBlockNumber(); } - /** - * Returns the retiring federation bitcoin address. - * @return the retiring federation bitcoin address, null if no retiring federation exists - */ public Address getRetiringFederationAddress() { return federationSupport.getRetiringFederationAddress(); } - /** - * Returns the retiring federation's size - * @return the retiring federation size, -1 if no retiring federation exists - */ public Integer getRetiringFederationSize() { return federationSupport.getRetiringFederationSize(); } - /** - * Returns the retiring federation's minimum required signatures - * @return the retiring federation minimum required signatures, -1 if no retiring federation exists - */ public Integer getRetiringFederationThreshold() { return federationSupport.getRetiringFederationThreshold(); } - /** - * Returns the public key of the retiring federation's federator at the given index - * @param index the retiring federator's index (zero-based) - * @return the retiring federator's public key, null if no retiring federation exists - */ public byte[] getRetiringFederatorBtcPublicKey(int index) { return federationSupport.getRetiringFederatorBtcPublicKey(index); } - /** - * Returns the public key of the given type of the retiring federation's federator at the given index - * @param index the retiring federator's index (zero-based) - * @param keyType the key type - * @return the retiring federator's public key of the given type, null if no retiring federation exists - */ public byte[] getRetiringFederatorPublicKeyOfType(int index, FederationMember.KeyType keyType) { return federationSupport.getRetiringFederatorPublicKeyOfType(index, keyType); } - /** - * Returns the retiring federation's creation time - * @return the retiring federation creation time, null if no retiring federation exists - */ public Instant getRetiringFederationCreationTime() { return federationSupport.getRetiringFederationCreationTime(); } - /** - * Returns the retiring federation's creation block number - * @return the retiring federation creation block number, - * -1 if no retiring federation exists - */ public long getRetiringFederationCreationBlockNumber() { return federationSupport.getRetiringFederationCreationBlockNumber(); } - /** - * Returns the currently live federations - * This would be the active federation plus - * potentially the retiring federation - * @return a list of live federations - */ - private List getLiveFederations() { - List liveFederations = new ArrayList<>(); - liveFederations.add(getActiveFederation()); - Federation retiringFederation = getRetiringFederation(); - if (retiringFederation != null) { - liveFederations.add(retiringFederation); - } - return liveFederations; - } - public Integer voteFederationChange(Transaction tx, ABICallSpec callSpec) { return federationSupport.voteFederationChange(tx, callSpec, signatureCache, eventLogger); } - /** - * Returns the currently pending federation hash, or null if none exists - * @return the currently pending federation hash, or null if none exists - */ public Keccak256 getPendingFederationHash() { return federationSupport.getPendingFederationHash(); } - /** - * Returns the currently pending federation size, or -1 if none exists - * @return the currently pending federation size, or -1 if none exists - */ public Integer getPendingFederationSize() { return federationSupport.getPendingFederationSize(); } - /** - * Returns the currently pending federation federator's public key at the given index, or null if none exists - * @param index the federator's index (zero-based) - * @return the pending federation's federator public key - */ public byte[] getPendingFederatorBtcPublicKey(int index) { return federationSupport.getPendingFederatorBtcPublicKey(index); } - /** - * Returns the public key of the given type of the pending federation's federator at the given index - * @param index the federator's index (zero-based) - * @param keyType the key type - * @return the pending federation's federator public key of given type - */ public byte[] getPendingFederatorPublicKeyOfType(int index, FederationMember.KeyType keyType) { return federationSupport.getPendingFederatorPublicKeyOfType(index, keyType); } @@ -2452,11 +2421,6 @@ public Coin getLockingCap() { return lockingCapSupport.getLockingCap().orElse(null); } - /** - * Returns the redeemScript of the current active federation - * - * @return Returns the redeemScript of the current active federation - */ public Optional