diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java index 3258071da5..3e88630104 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java @@ -1567,50 +1567,46 @@ private void adjustBalancesIfChangeOutputWasDust(BtcTransaction btcTx, Coin sent * The hash for the signature must be calculated with Transaction.SigHash.ALL and anyoneCanPay=false. The signature must be canonical. * If enough signatures were added, ask federators to broadcast the btc release tx. * - * @param federatorBtcPublicKey Federator who is signing - * @param signatures 1 signature per btc tx input - * @param rskTxHash The hash of the rsk tx + * @param federatorBtcPublicKey Federator who is signing + * @param signatures 1 signature per btc tx input + * @param releaseCreationRskTxHash The hash of the release creation rsk tx */ - public void addSignature(BtcECKey federatorBtcPublicKey, List signatures, Keccak256 rskTxHash) throws IOException { + public void addSignature(BtcECKey federatorBtcPublicKey, List signatures, Keccak256 releaseCreationRskTxHash) throws IOException { if (signatures == null || signatures.isEmpty()) { return; } Context.propagate(btcContext); - BtcTransaction releaseTx = provider.getPegoutsWaitingForSignatures().get(rskTxHash); - if (releaseTx == null) { - logger.warn("No tx waiting for signature for hash {}. Probably fully signed already.", rskTxHash); - return; - } - if (!hasEnoughSignatures(releaseTx, signatures)) { + if (svpIsOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) { + logger.trace("[addSignature] Going to sign svp spend transaction with federator public key {}", federatorBtcPublicKey); + addSvpSpendTxSignatures(federatorBtcPublicKey, signatures); return; } - addReleaseSignatures(federatorBtcPublicKey, signatures, rskTxHash, releaseTx); - } - - private boolean hasEnoughSignatures(BtcTransaction releaseTx, List signatures) { - int inputsSize = releaseTx.getInputs().size(); - int signaturesSize = signatures.size(); - - if (inputsSize != signaturesSize) { - logger.warn("Expected {} signatures but received {}.", inputsSize, signaturesSize); - return false; - } - return true; + logger.trace("[addSignature] Going to sign release transaction with federator public key {}", federatorBtcPublicKey); + addReleaseSignatures(federatorBtcPublicKey, signatures, releaseCreationRskTxHash); } private void addReleaseSignatures( BtcECKey federatorPublicKey, List signatures, - Keccak256 rskTxHash, - BtcTransaction btcTx + Keccak256 releaseCreationRskTxHash ) throws IOException { + + BtcTransaction releaseTx = provider.getPegoutsWaitingForSignatures().get(releaseCreationRskTxHash); + if (releaseTx == null) { + logger.warn("[addReleaseSignatures] No tx waiting for signature for hash {}. Probably fully signed already.", releaseCreationRskTxHash); + return; + } + if (!areSignaturesEnoughToSignAllTxInputs(releaseTx, signatures)) { + return; + } + Optional optionalFederation = getFederationFromPublicKey(federatorPublicKey); if (optionalFederation.isEmpty()) { logger.warn( - "[addSignature] Supplied federator btc public key {} does not belong to any of the federators.", + "[addReleaseSignatures] Supplied federator btc public key {} does not belong to any of the federators.", federatorPublicKey ); return; @@ -1620,27 +1616,27 @@ private void addReleaseSignatures( Optional federationMember = federation.getMemberByBtcPublicKey(federatorPublicKey); if (federationMember.isEmpty()){ logger.warn( - "[addSignature] Supplied federator btc public key {} doest not match any of the federator member btc public keys {}.", + "[addReleaseSignatures] Supplied federator btc public key {} doest not match any of the federator member btc public keys {}.", federatorPublicKey, federation.getBtcPublicKeys() ); return; } FederationMember signingFederationMember = federationMember.get(); - byte[] rskTxHashSerialized = rskTxHash.getBytes(); + byte[] releaseCreationRskTxHashSerialized = releaseCreationRskTxHash.getBytes(); if (!activations.isActive(ConsensusRule.RSKIP326)) { - eventLogger.logAddSignature(signingFederationMember, btcTx, rskTxHashSerialized); + eventLogger.logAddSignature(signingFederationMember, releaseTx, releaseCreationRskTxHashSerialized); } - processSigning(signingFederationMember, signatures, rskTxHash, btcTx); + processSigning(signingFederationMember, signatures, releaseCreationRskTxHash, releaseTx); - if (!BridgeUtils.hasEnoughSignatures(btcContext, btcTx)) { - logMissingSignatures(btcTx, rskTxHash, federation); + if (!BridgeUtils.hasEnoughSignatures(btcContext, releaseTx)) { + logMissingSignatures(releaseTx, releaseCreationRskTxHash, federation); return; } - logReleaseBtc(btcTx, rskTxHashSerialized); - provider.getPegoutsWaitingForSignatures().remove(rskTxHash); + logReleaseBtc(releaseTx, releaseCreationRskTxHashSerialized); + provider.getPegoutsWaitingForSignatures().remove(releaseCreationRskTxHash); } private Optional getFederationFromPublicKey(BtcECKey federatorPublicKey) { @@ -1657,24 +1653,75 @@ private Optional getFederationFromPublicKey(BtcECKey federatorPublic return Optional.empty(); } - private void logMissingSignatures(BtcTransaction btcTx, Keccak256 rskTxHash, Federation federation) { + private boolean isSvpSpendTx(Keccak256 releaseCreationRskTxHash) { + return provider.getSvpSpendTxWaitingForSignatures() + .map(Map.Entry::getKey) + .filter(key -> key.equals(releaseCreationRskTxHash)) + .isPresent(); + } + + private void addSvpSpendTxSignatures( + BtcECKey proposedFederatorPublicKey, + List signatures + ) { + 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.")); + 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(); + + if (!areSignaturesEnoughToSignAllTxInputs(svpSpendTx, signatures)) { + return; + } + + processSigning(federationMember, signatures, svpSpendTxCreationRskTxHash, svpSpendTx); + + if (!BridgeUtils.hasEnoughSignatures(btcContext, svpSpendTx)) { + logMissingSignatures(svpSpendTx, svpSpendTxCreationRskTxHash, proposedFederation); + return; + } + + logReleaseBtc(svpSpendTx, svpSpendTxCreationRskTxHash.getBytes()); + provider.setSvpSpendTxWaitingForSignatures(null); + }); + } + + private boolean areSignaturesEnoughToSignAllTxInputs(BtcTransaction releaseTx, List signatures) { + int inputsSize = releaseTx.getInputs().size(); + int signaturesSize = signatures.size(); + + if (inputsSize != signaturesSize) { + logger.warn("[areSignaturesEnoughToSignAllTxInputs] Expected {} signatures but received {}.", inputsSize, signaturesSize); + return false; + } + return true; + } + + private void logMissingSignatures(BtcTransaction btcTx, Keccak256 releaseCreationRskTxHash, Federation federation) { int missingSignatures = BridgeUtils.countMissingSignatures(btcContext, btcTx); int neededSignatures = federation.getNumberOfSignaturesRequired(); int signaturesCount = neededSignatures - missingSignatures; - logger.debug("Tx {} not yet fully signed. Requires {}/{} signatures but has {}", - rskTxHash, neededSignatures, federation.getSize(), signaturesCount); + logger.debug("[logMissingSignatures] Tx {} not yet fully signed. Requires {}/{} signatures but has {}", + releaseCreationRskTxHash, neededSignatures, federation.getSize(), signaturesCount); } - private void logReleaseBtc(BtcTransaction btcTx, byte[] rskTxHashSerialized) { - logger.info("Tx fully signed {}. Hex: {}", btcTx, Bytes.of(btcTx.bitcoinSerialize())); - eventLogger.logReleaseBtc(btcTx, rskTxHashSerialized); + private void logReleaseBtc(BtcTransaction btcTx, byte[] releaseCreationRskTxHashSerialized) { + logger.info("[logReleaseBtc] Tx fully signed {}. Hex: {}", btcTx, Bytes.of(btcTx.bitcoinSerialize())); + eventLogger.logReleaseBtc(btcTx, releaseCreationRskTxHashSerialized); } private void processSigning( FederationMember federatorMember, List signatures, - Keccak256 rskTxHash, + Keccak256 releaseCreationRskTxHash, BtcTransaction btcTx) { // Build input hashes for signatures @@ -1695,10 +1742,10 @@ private void processSigning( } // All signatures are correct. Proceed to signing - boolean signed = sign(federatorBtcPublicKey, txSigs, sigHashes, rskTxHash, btcTx); + boolean signed = sign(federatorBtcPublicKey, txSigs, sigHashes, releaseCreationRskTxHash, btcTx); if (signed && activations.isActive(ConsensusRule.RSKIP326)) { - eventLogger.logAddSignature(federatorMember, btcTx, rskTxHash.getBytes()); + eventLogger.logAddSignature(federatorMember, btcTx, releaseCreationRskTxHash.getBytes()); } } @@ -1712,7 +1759,7 @@ private List getTransactionSignatures(BtcECKey federatorBt if (!federatorBtcPublicKey.verify(sigHash, decodedSignature)) { logger.warn( - "Signature {} {} is not valid for hash {} and public key {}", + "[getTransactionSignatures] Signature {} {} is not valid for hash {} and public key {}", i, Bytes.of(decodedSignature.encodeToDER()), sigHash, @@ -1723,7 +1770,7 @@ private List getTransactionSignatures(BtcECKey federatorBt TransactionSignature txSig = new TransactionSignature(decodedSignature, BtcTransaction.SigHash.ALL, false); if (!txSig.isCanonical()) { - logger.warn("Signature {} {} is not canonical.", i, Bytes.of(decodedSignature.encodeToDER())); + logger.warn("[getTransactionSignatures] Signature {} {} is not canonical.", i, Bytes.of(decodedSignature.encodeToDER())); throw new SignatureException(); } txSigs.add(txSig); @@ -1738,7 +1785,7 @@ private List getDecodedSignatures(List signatur decodedSignatures.add(BtcECKey.ECDSASignature.decodeFromDER(signature)); } catch (RuntimeException e) { int index = signatures.indexOf(signature); - logger.warn("Malformed signature for input {} : {}", index, Bytes.of(signature)); + logger.warn("[getDecodedSignatures] Malformed signature for input {} : {}", index, Bytes.of(signature)); throw new SignatureException(); } } @@ -1749,7 +1796,7 @@ private boolean sign( BtcECKey federatorBtcPublicKey, List txSigs, List sigHashes, - Keccak256 rskTxHash, + Keccak256 releaseCreationRskTxHash, BtcTransaction btcTx) { boolean signed = false; @@ -1762,7 +1809,7 @@ private boolean sign( BridgeUtils.isInputSignedByThisFederator(federatorBtcPublicKey, sigHash, input); if (alreadySignedByThisFederator) { - logger.warn("Input {} of tx {} already signed by this federator.", i, rskTxHash); + logger.warn("[sign] Input {} of tx {} already signed by this federator.", i, releaseCreationRskTxHash); break; } @@ -1778,14 +1825,14 @@ private boolean sign( Script inputScriptWithSignature = outputScript.getScriptSigWithSignature(inputScript, txSigs.get(i).encodeToBitcoin(), sigIndex); input.setScriptSig(inputScriptWithSignature); - logger.debug("Tx input {} for tx {} signed.", i, rskTxHash); + logger.debug("[sign] Tx input {} for tx {} signed.", i, releaseCreationRskTxHash); signed = true; } catch (IllegalStateException e) { Federation retiringFederation = getRetiringFederation(); if (getActiveFederation().hasBtcPublicKey(federatorBtcPublicKey)) { - logger.debug("A member of the active federation is trying to sign a tx of the retiring one"); + logger.debug("[sign] A member of the active federation is trying to sign a tx of the retiring one"); } else if (retiringFederation != null && retiringFederation.hasBtcPublicKey(federatorBtcPublicKey)) { - logger.debug("A member of the retiring federation is trying to sign a tx of the active one"); + logger.debug("[sign] A member of the retiring federation is trying to sign a tx of the active one"); } return false; } diff --git a/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java b/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java index bbce67d2ad..0290676b0d 100644 --- a/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java +++ b/rskj-core/src/test/java/co/rsk/peg/BridgeSupportSvpTest.java @@ -1,7 +1,9 @@ package co.rsk.peg; +import co.rsk.RskTestUtils; import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.script.Script; +import co.rsk.bitcoinj.script.ScriptBuilder; import co.rsk.bitcoinj.script.ScriptChunk; import co.rsk.bitcoinj.store.BlockStoreException; import co.rsk.core.RskAddress; @@ -11,10 +13,7 @@ import co.rsk.peg.bitcoin.UtxoUtils; import co.rsk.peg.constants.BridgeConstants; import co.rsk.peg.constants.BridgeMainNetConstants; -import co.rsk.peg.federation.Federation; -import co.rsk.peg.federation.FederationSupport; -import co.rsk.peg.federation.FederationTestUtils; -import co.rsk.peg.federation.P2shErpFederationBuilder; +import co.rsk.peg.federation.*; import co.rsk.peg.federation.constants.FederationConstants; import co.rsk.peg.feeperkb.FeePerKbSupport; import co.rsk.peg.utils.BridgeEventLogger; @@ -23,6 +22,8 @@ import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.config.blockchain.upgrades.ActivationConfigsForTest; import org.ethereum.core.*; +import org.ethereum.crypto.ECKey; +import org.ethereum.util.ByteUtil; import org.ethereum.vm.DataWord; import org.ethereum.vm.LogInfo; import org.ethereum.vm.PrecompiledContracts; @@ -32,10 +33,14 @@ import java.util.*; import java.util.stream.IntStream; +import static co.rsk.RskTestUtils.createRskBlock; import static co.rsk.peg.BridgeSupportTestUtil.*; import static co.rsk.peg.PegUtils.getFlyoverRedeemScript; -import static co.rsk.peg.bitcoin.BitcoinUtils.createBaseP2SHInputScriptThatSpendsFromRedeemScript; -import static co.rsk.peg.bitcoin.BitcoinUtils.searchForOutput; +import static co.rsk.peg.ReleaseTransactionBuilder.BTC_TX_VERSION_2; +import static co.rsk.peg.bitcoin.BitcoinTestUtils.generateSignerEncodedSignatures; +import static co.rsk.peg.bitcoin.BitcoinTestUtils.generateTransactionInputsSigHashes; +import static co.rsk.peg.bitcoin.BitcoinUtils.*; +import static co.rsk.peg.bitcoin.BitcoinUtils.addInputFromMatchingOutputScript; import static co.rsk.peg.bitcoin.UtxoUtils.extractOutpointValues; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @@ -45,6 +50,8 @@ public class BridgeSupportSvpTest { private static final RskAddress bridgeContractAddress = PrecompiledContracts.BRIDGE_ADDR; private final CallTransaction.Function releaseRequestedEvent = BridgeEvents.RELEASE_REQUESTED.getEvent(); private final CallTransaction.Function pegoutTransactionCreatedEvent = BridgeEvents.PEGOUT_TRANSACTION_CREATED.getEvent(); + private final CallTransaction.Function addSignatureEvent = BridgeEvents.ADD_SIGNATURE.getEvent(); + private final CallTransaction.Function releaseBtcEvent = BridgeEvents.RELEASE_BTC.getEvent(); private static final ActivationConfig.ForBlock allActivations = ActivationConfigsForTest.all().forBlock(0); private static final BridgeConstants bridgeMainNetConstants = BridgeMainNetConstants.getInstance(); @@ -314,15 +321,7 @@ private void assertSvpFundTransactionValuesWereNotUpdated() { @Test void registerBtcTransaction_forSvpFundTransactionChange_whenValidationPeriodEnded_shouldRegisterTransactionButNotUpdateSvpFundTransactionValues() throws Exception { // arrange - // make rsk execution block to be after validation period ended - long validationPeriodEndBlock = proposedFederation.getCreationBlockNumber() - + bridgeMainNetConstants.getFederationConstants().getValidationPeriodDurationInBlocks(); - long rskExecutionBlockTimestamp = 10L; - BlockHeader blockHeader = new BlockHeaderBuilder(mock(ActivationConfig.class)) - .setNumber(validationPeriodEndBlock + 1) // adding one more block to ensure validation period is ended - .setTimestamp(rskExecutionBlockTimestamp) - .build(); - rskExecutionBlock = Block.createBlockFromHeader(blockHeader, true); + arrangeExecutionBlockIsAfterValidationPeriodEnded(); BtcTransaction svpFundTransaction = arrangeSvpFundTransactionUnsignedWithChange(); signInputs(svpFundTransaction); // a transaction trying to be registered should be signed @@ -349,7 +348,7 @@ void registerBtcTransaction_forNormalPegout_whenSvpPeriodIsOngoing_shouldRegiste // Arrange arrangeSvpFundTransactionUnsignedWithChange(); - BtcTransaction pegout = createPegout(); + BtcTransaction pegout = createPegout(proposedFederation.getRedeemScript()); savePegoutIndex(pegout); signInputs(pegout); // a transaction trying to be registered should be signed setUpForTransactionRegistration(pegout); @@ -437,21 +436,6 @@ private void saveSvpFundTransactionHashUnsigned(Sha256Hash svpFundTransactionHas bridgeSupport.save(); } - private BtcTransaction createPegout() { - BtcTransaction pegout = new BtcTransaction(btcMainnetParams); - Sha256Hash parentTxHash = BitcoinTestUtils.createHash(2); - addInput(pegout, parentTxHash); - addOutputChange(pegout); - - return pegout; - } - - private void addOutputChange(BtcTransaction transaction) { - // add output to the active fed - Script activeFederationP2SHScript = activeFederation.getP2SHScript(); - transaction.addOutput(Coin.COIN.multiply(10), activeFederationP2SHScript); - } - private void savePegoutIndex(BtcTransaction pegout) { BitcoinUtils.getFirstInputSigHash(pegout) .ifPresent(inputSigHash -> bridgeStorageProvider.setPegoutTxSigHash(inputSigHash)); @@ -569,7 +553,7 @@ private void assertSvpSpendTransactionReleaseWasNotLogged() { @Test void processSvpSpendTransaction_createsExpectedTransactionAndSavesTheValuesAndLogsExpectedEvents() { // arrange - arrangeSvpFundTransactionSigned(); + svpFundTransaction = arrangeSvpFundTransactionSigned(); // act bridgeSupport.processSvpSpendTransactionUnsigned(rskTx); @@ -587,14 +571,6 @@ void processSvpSpendTransaction_createsExpectedTransactionAndSavesTheValuesAndLo assertLogReleaseRequested(logs, rskTx.getHash(), svpSpendTransactionHashUnsigned, valueSentToActiveFed); } - private void arrangeSvpFundTransactionSigned() { - svpFundTransaction = recreateSvpFundTransactionUnsigned(); - signInputs(svpFundTransaction); - - bridgeStorageProvider.setSvpFundTxSigned(svpFundTransaction); - bridgeStorageProvider.save(); - } - private void assertSvpSpendTxHashUnsignedWasSavedInStorage() { Optional svpSpendTransactionHashUnsignedOpt = bridgeStorageProvider.getSvpSpendTxHashUnsigned(); assertTrue(svpSpendTransactionHashUnsignedOpt.isPresent()); @@ -662,11 +638,314 @@ private void assertInputHasExpectedScriptSig(TransactionInput input, Script rede } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Tag("svp spend transaction signing tests") + class SvpSpendTxSigning { + private static final List PROPOSED_FEDERATION_SIGNERS_KEYS = + BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"member01", "member02", "member03", "member04", "member05"}, true); // this is needed to have the private keys too + + private static final Keccak256 svpSpendTxCreationHash = RskTestUtils.createHash(1); + + private BtcTransaction svpSpendTx; + private List svpSpendTxSigHashes; + private List logs; + private BridgeEventLogger bridgeEventLogger; + + @BeforeEach + void setUp() { + logs = new ArrayList<>(); + bridgeEventLogger = new BridgeEventLoggerImpl( + bridgeMainNetConstants, + allActivations, + logs + ); + + bridgeSupport = bridgeSupportBuilder + .withBridgeConstants(bridgeMainNetConstants) + .withProvider(bridgeStorageProvider) + .withActivations(allActivations) + .withFederationSupport(federationSupport) + .withFeePerKbSupport(feePerKbSupport) + .withEventLogger(bridgeEventLogger) + .withExecutionBlock(rskExecutionBlock) + .build(); + + arrangeSvpSpendTransaction(); + svpSpendTxSigHashes = generateTransactionInputsSigHashes(svpSpendTx); + } + + @Test + void addSignature_forSvpSpendTx_withWrongKeys_shouldThrowIllegalStateExceptionAndNotAddProposedFederatorSignatures() { + // arrange + List wrongSigningKeys = + BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"wrong01", "wrong02", "wrong03", "wrong04", "wrong05"}, true); + + // act & assert + for (BtcECKey wrongSigningKey : wrongSigningKeys) { + List signatures = generateSignerEncodedSignatures(wrongSigningKey, svpSpendTxSigHashes); + assertThrows(IllegalStateException.class, + () -> bridgeSupport.addSignature(wrongSigningKey, signatures, svpSpendTxCreationHash)); + } + + // assert + for (BtcECKey wrongSigningKey : wrongSigningKeys) { + assertFederatorDidNotSignInputs(svpSpendTx.getInputs(), svpSpendTxSigHashes, wrongSigningKey); + } + + assertAddSignatureWasNotLogged(); + assertSvpSpendTxWFSWasNotRemoved(); + } + + @Test + void addSignature_forSvpSpendTx_whenProposedFederationDoesNotExist_shouldNotAddProposedFederatorSignatures() throws Exception { + // arrange + when(federationSupport.getProposedFederation()).thenReturn(Optional.empty()); + + // act + for (BtcECKey proposedFederatorSignerKey : PROPOSED_FEDERATION_SIGNERS_KEYS) { + List signatures = generateSignerEncodedSignatures(proposedFederatorSignerKey, svpSpendTxSigHashes); + bridgeSupport.addSignature(proposedFederatorSignerKey, signatures, svpSpendTxCreationHash); + } + + // assert + for (BtcECKey key : PROPOSED_FEDERATION_SIGNERS_KEYS) { + assertFederatorDidNotSignInputs(svpSpendTx.getInputs(), svpSpendTxSigHashes, key); + } + + assertAddSignatureWasNotLogged(); + assertSvpSpendTxWFSWasNotRemoved(); + } + + @Test + void addSignature_forSvpSpendTx_whenValidationPeriodEnded_shouldNotAddProposedFederatorsSignatures() throws Exception { + // arrange + arrangeExecutionBlockIsAfterValidationPeriodEnded(); + bridgeSupport = bridgeSupportBuilder + .withBridgeConstants(bridgeMainNetConstants) + .withProvider(bridgeStorageProvider) + .withActivations(allActivations) + .withFederationSupport(federationSupport) + .withFeePerKbSupport(feePerKbSupport) + .withEventLogger(bridgeEventLogger) + .withExecutionBlock(rskExecutionBlock) + .build(); + + // act + for (BtcECKey proposedFederatorSignerKeys : PROPOSED_FEDERATION_SIGNERS_KEYS) { + List signatures = generateSignerEncodedSignatures(proposedFederatorSignerKeys, svpSpendTxSigHashes); + bridgeSupport.addSignature(proposedFederatorSignerKeys, signatures, svpSpendTxCreationHash); + } + + // assert + for (BtcECKey proposedFederatorSignerKeys : PROPOSED_FEDERATION_SIGNERS_KEYS) { + assertFederatorDidNotSignInputs(svpSpendTx.getInputs(), svpSpendTxSigHashes, proposedFederatorSignerKeys); + } + + assertAddSignatureWasNotLogged(); + assertSvpSpendTxWFSWasNotRemoved(); + } + + @Test + void addSignature_forSvpSpendTx_withoutEnoughSignatures_shouldNotAddProposedFederatorsSignatures() throws Exception { + // act + for (BtcECKey proposedFederatorSignerKey : PROPOSED_FEDERATION_SIGNERS_KEYS) { + List signatures = generateSignerEncodedSignatures(proposedFederatorSignerKey, svpSpendTxSigHashes); + List notEnoughSignatures = signatures.subList(0, signatures.size() - 1); + bridgeSupport.addSignature(proposedFederatorSignerKey, notEnoughSignatures, svpSpendTxCreationHash); + } + + // assert + for (BtcECKey proposedFederatorSignerKeys : PROPOSED_FEDERATION_SIGNERS_KEYS) { + assertFederatorDidNotSignInputs(svpSpendTx.getInputs(), svpSpendTxSigHashes, proposedFederatorSignerKeys); + } + + assertAddSignatureWasNotLogged(); + assertSvpSpendTxWFSWasNotRemoved(); + } + + private void assertFederatorDidNotSignInputs(List inputs, List sigHashes, BtcECKey key) { + for (TransactionInput input : inputs) { + Sha256Hash sigHash = sigHashes.get(inputs.indexOf(input)); + assertFalse(BridgeUtils.isInputSignedByThisFederator(key, sigHash, input)); + } + } + + private void assertAddSignatureWasNotLogged() { + assertEquals(0, logs.size()); + } + + private void assertSvpSpendTxWFSWasNotRemoved() { + assertTrue(bridgeStorageProvider.getSvpSpendTxWaitingForSignatures().isPresent()); + } + + @Test + void addSignature_forSvpSpendTx_shouldAddProposedFederatorsSignatures() throws Exception { + // act + for (BtcECKey proposedFederatorSignerKey : PROPOSED_FEDERATION_SIGNERS_KEYS) { + List signatures = generateSignerEncodedSignatures(proposedFederatorSignerKey, svpSpendTxSigHashes); + bridgeSupport.addSignature(proposedFederatorSignerKey, signatures, svpSpendTxCreationHash); + } + + // assert + for (BtcECKey proposedFederatorSignerKey : PROPOSED_FEDERATION_SIGNERS_KEYS) { + assertFederatorSigning( + svpSpendTxCreationHash.getBytes(), + svpSpendTx.getInputs(), + svpSpendTxSigHashes, + proposedFederation, + proposedFederatorSignerKey + ); + } + assertLogReleaseBtc(svpSpendTxCreationHash, svpSpendTx); + assertLogsSize(PROPOSED_FEDERATION_SIGNERS_KEYS.size() + 1); // proposedFedSigners size for addSignature, +1 for release btc + assertFalse(bridgeStorageProvider.getSvpSpendTxWaitingForSignatures().isPresent()); + } + + @Test + void addSignature_forNormalPegout_whenSvpIsOngoing_shouldAddJustActiveFederatorsSignaturesToPegout() throws Exception { + Keccak256 rskTxHash = RskTestUtils.createHash(2); + + BtcTransaction pegout = createPegout(activeFederation.getRedeemScript()); + SortedMap pegoutsWFS = bridgeStorageProvider.getPegoutsWaitingForSignatures(); + pegoutsWFS.put(rskTxHash, pegout); + + List activeFedSignersKeys = + BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"fa01", "fa02", "fa03", "fa04", "fa05"}, true); + + List pegoutTxSigHashes = generateTransactionInputsSigHashes(pegout); + + // act + for (BtcECKey activeFedSignerKey : activeFedSignersKeys) { + List signatures = generateSignerEncodedSignatures(activeFedSignerKey, pegoutTxSigHashes); + bridgeSupport.addSignature(activeFedSignerKey, signatures, rskTxHash); + } + + // assert + List pegoutInputs = pegout.getInputs(); + for (BtcECKey key : activeFedSignersKeys) { + assertFederatorSigning( + rskTxHash.getBytes(), + pegout.getInputs(), + pegoutTxSigHashes, + activeFederation, + key + ); + } + + assertLogReleaseBtc(rskTxHash, pegout); + assertLogsSize(activeFedSignersKeys.size() + 1); // activeFedSignersKeys size for addSignature, +1 for release btc + + for (BtcECKey key : PROPOSED_FEDERATION_SIGNERS_KEYS) { + assertFederatorDidNotSignInputs(pegoutInputs, pegoutTxSigHashes, key); + } + assertSvpSpendTxWFSWasNotRemoved(); + } + + private void assertLogsSize(int expectedLogs) { + assertEquals(expectedLogs, logs.size()); + } + + private void assertFederatorSigning( + byte[] rskTxHashSerialized, + List inputs, + List sigHashes, + Federation federation, + BtcECKey key + ) { + Optional federationMember = federation.getMemberByBtcPublicKey(key); + assertTrue(federationMember.isPresent()); + assertLogAddSignature(federationMember.get(), rskTxHashSerialized); + assertFederatorSignedInputs(inputs, sigHashes, key); + } + + private void assertLogAddSignature(FederationMember federationMember, byte[] rskTxHash) { + ECKey federatorRskPublicKey = federationMember.getRskPublicKey(); + String federatorRskAddress = ByteUtil.toHexString(federatorRskPublicKey.getAddress()); + + List encodedTopics = getEncodedTopics(addSignatureEvent, rskTxHash, federatorRskAddress); + + BtcECKey federatorBtcPublicKey = federationMember.getBtcPublicKey(); + byte[] encodedData = getEncodedData(addSignatureEvent, federatorBtcPublicKey.getPubKey()); + + assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); + assertEventWasEmittedWithExpectedData(logs, encodedData); + } + + private void assertLogReleaseBtc(Keccak256 rskTxHash, BtcTransaction btcTx) { + byte[] rskTxHashSerialized = rskTxHash.getBytes(); + List encodedTopics = getEncodedTopics(releaseBtcEvent, rskTxHashSerialized); + + byte[] btcTxSerialized = btcTx.bitcoinSerialize(); + byte[] encodedData = getEncodedData(releaseBtcEvent, btcTxSerialized); + + assertEventWasEmittedWithExpectedTopics(logs, encodedTopics); + assertEventWasEmittedWithExpectedData(logs, encodedData); + } + + private void assertFederatorSignedInputs(List inputs, List sigHashes, BtcECKey key) { + for (TransactionInput input : inputs) { + Sha256Hash sigHash = sigHashes.get(inputs.indexOf(input)); + assertTrue(BridgeUtils.isInputSignedByThisFederator(key, sigHash, input)); + } + } + + private void arrangeSvpSpendTransaction() { + recreateSvpSpendTransaction(); + saveSvpSpendTransactionWFSValues(); + } + + private void recreateSvpSpendTransaction() { + svpSpendTx = new BtcTransaction(btcMainnetParams); + svpSpendTx.setVersion(BTC_TX_VERSION_2); + + BtcTransaction svpFundTx = arrangeSvpFundTransactionSigned(); + // add inputs + addInputFromMatchingOutputScript(svpSpendTx, svpFundTx, proposedFederation.getP2SHScript()); + Script proposedFederationRedeemScript = proposedFederation.getRedeemScript(); + svpSpendTx.getInput(0) + .setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(proposedFederationRedeemScript)); + + Script flyoverRedeemScript = getFlyoverRedeemScript(bridgeMainNetConstants.getProposedFederationFlyoverPrefix(), proposedFederationRedeemScript); + addInputFromMatchingOutputScript(svpSpendTx, svpFundTx, ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript)); + svpSpendTx.getInput(1) + .setScriptSig(createBaseP2SHInputScriptThatSpendsFromRedeemScript(flyoverRedeemScript)); + + // add output + svpSpendTx.addOutput(Coin.valueOf(1762), federationSupport.getActiveFederationAddress()); + } + + private void saveSvpSpendTransactionWFSValues() { + Map.Entry svpSpendTxWaitingForSignatures = new AbstractMap.SimpleEntry<>(svpSpendTxCreationHash, svpSpendTx); + bridgeStorageProvider.setSvpSpendTxWaitingForSignatures(svpSpendTxWaitingForSignatures); + bridgeStorageProvider.save(); + } + } + + private void arrangeExecutionBlockIsAfterValidationPeriodEnded() { + long validationPeriodEndBlock = proposedFederation.getCreationBlockNumber() + + bridgeMainNetConstants.getFederationConstants().getValidationPeriodDurationInBlocks(); + long rskExecutionBlockNumber = validationPeriodEndBlock + 1; // adding one more block to ensure validation period is ended + long rskExecutionBlockTimestamp = 10L; + + rskExecutionBlock = createRskBlock(rskExecutionBlockNumber, rskExecutionBlockTimestamp); + } + + private BtcTransaction arrangeSvpFundTransactionSigned() { + BtcTransaction svpFundTransaction = recreateSvpFundTransactionUnsigned(); + signInputs(svpFundTransaction); + + bridgeStorageProvider.setSvpFundTxSigned(svpFundTransaction); + bridgeStorageProvider.save(); + + return svpFundTransaction; + } + private BtcTransaction recreateSvpFundTransactionUnsigned() { BtcTransaction svpFundTransaction = new BtcTransaction(btcMainnetParams); Sha256Hash parentTxHash = BitcoinTestUtils.createHash(1); - addInput(svpFundTransaction, parentTxHash); + addInput(svpFundTransaction, parentTxHash, proposedFederation.getRedeemScript()); svpFundTransaction.addOutput(spendableValueFromProposedFederation, proposedFederation.getAddress()); Address flyoverProposedFederationAddress = @@ -676,17 +955,30 @@ private BtcTransaction recreateSvpFundTransactionUnsigned() { return svpFundTransaction; } - private void addInput(BtcTransaction transaction, Sha256Hash parentTxHash) { - // we need to add an input that we can actually sign, - // and we know the private keys for the following scriptSig - Federation federation = P2shErpFederationBuilder.builder().build(); + private BtcTransaction createPegout(Script redeemScript) { + BtcTransaction pegout = new BtcTransaction(btcMainnetParams); + Sha256Hash parentTxHash = BitcoinTestUtils.createHash(2); + addInput(pegout, parentTxHash, redeemScript); + addOutputChange(pegout); + + return pegout; + } + + private void addInput(BtcTransaction transaction, Sha256Hash parentTxHash, Script redeemScript) { + // we need to add an input that we can actually sign transaction.addInput( parentTxHash, 0, - createBaseP2SHInputScriptThatSpendsFromRedeemScript(federation.getRedeemScript()) + createBaseP2SHInputScriptThatSpendsFromRedeemScript(redeemScript) ); } + private void addOutputChange(BtcTransaction transaction) { + // add output to the active fed + Script activeFederationP2SHScript = activeFederation.getP2SHScript(); + transaction.addOutput(Coin.COIN.multiply(10), activeFederationP2SHScript); + } + private void signInputs(BtcTransaction transaction) { List keysToSign = BitcoinTestUtils.getBtcEcKeysFromSeeds(new String[]{"member01", "member02", "member03", "member04", "member05"}, true); diff --git a/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinTestUtils.java b/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinTestUtils.java index 146d389f73..bd1cf8007c 100644 --- a/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinTestUtils.java +++ b/rskj-core/src/test/java/co/rsk/peg/bitcoin/BitcoinTestUtils.java @@ -2,6 +2,7 @@ import static co.rsk.bitcoinj.script.ScriptBuilder.createP2SHOutputScript; import static co.rsk.peg.bitcoin.BitcoinUtils.extractRedeemScriptFromInput; +import static co.rsk.peg.bitcoin.BitcoinUtils.generateSigHashForP2SHTransactionInput; import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.crypto.TransactionSignature; @@ -9,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.bouncycastle.util.encoders.Hex; import org.ethereum.crypto.HashUtil; @@ -154,4 +156,17 @@ private static void signLegacyTransactionInputFromP2shMultiSig(BtcTransaction tr input.setScriptSig(inputScriptSig); } } + + public static List generateTransactionInputsSigHashes(BtcTransaction btcTx) { + return IntStream.range(0, btcTx.getInputs().size()) + .mapToObj(i -> generateSigHashForP2SHTransactionInput(btcTx, i)) + .toList(); + } + + public static List generateSignerEncodedSignatures(BtcECKey signingKey, List sigHashes) { + return sigHashes.stream() + .map(signingKey::sign) + .map(BtcECKey.ECDSASignature::encodeToDER) + .toList(); + } }