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 9e6144675ca..9d9663a4831 100644 --- a/rskj-core/src/main/java/co/rsk/peg/Bridge.java +++ b/rskj-core/src/main/java/co/rsk/peg/Bridge.java @@ -17,6 +17,7 @@ */ package co.rsk.peg; +import static co.rsk.peg.BridgeSerializationUtils.deserializeRskTxHash; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP417; import co.rsk.bitcoinj.core.*; @@ -33,6 +34,7 @@ import co.rsk.peg.vote.ABICallSpec; import co.rsk.peg.bitcoin.MerkleBranch; import co.rsk.peg.federation.Federation; +import co.rsk.peg.federation.FederationChangeResponseCode; import co.rsk.peg.federation.FederationMember; import co.rsk.peg.flyover.FlyoverTxResponseCodes; import co.rsk.peg.utils.BtcTransactionFormatUtils; @@ -95,6 +97,8 @@ public class Bridge extends PrecompiledContracts.PrecompiledContract { public static final CallTransaction.Function ADD_SIGNATURE = BridgeMethods.ADD_SIGNATURE.getFunction(); // Returns a StateForFederator encoded in RLP public static final CallTransaction.Function GET_STATE_FOR_BTC_RELEASE_CLIENT = BridgeMethods.GET_STATE_FOR_BTC_RELEASE_CLIENT.getFunction(); + // Returns a StateForProposedFederator encoded in RLP + public static final CallTransaction.Function GET_STATE_FOR_SVP_CLIENT = BridgeMethods.GET_STATE_FOR_SVP_CLIENT.getFunction(); // Returns a BridgeState encoded in RLP public static final CallTransaction.Function GET_STATE_FOR_DEBUGGING = BridgeMethods.GET_STATE_FOR_DEBUGGING.getFunction(); // Return the bitcoin blockchain best chain height know by the bridge contract @@ -152,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 @@ -614,14 +629,15 @@ public void addSignature(Object[] args) throws VMException { } signatures.add(signatureByteArray); } - byte[] rskTxHash = (byte[]) args[2]; - if (rskTxHash.length!=32) { - throw new BridgeIllegalArgumentException("Invalid rsk tx hash " + Bytes.of(rskTxHash)); + byte[] rskTxHashSerialized = (byte[]) args[2]; + Keccak256 rskTxHash; + try { + rskTxHash = deserializeRskTxHash(rskTxHashSerialized); + } catch (IllegalArgumentException e) { + throw new BridgeIllegalArgumentException("Invalid rsk tx hash " + Bytes.of(rskTxHashSerialized)); } try { bridgeSupport.addSignature(federatorPublicKey, signatures, rskTxHash); - } catch (BridgeIllegalArgumentException e) { - throw e; } catch (Exception e) { logger.warn("Exception in addSignature", e); throw new VMException("Exception in addSignature", e); @@ -639,6 +655,17 @@ public byte[] getStateForBtcReleaseClient(Object[] args) throws VMException { } } + public byte[] getStateForSvpClient(Object[] args) throws VMException { + logger.trace("getStateForSvpClient"); + + try { + return bridgeSupport.getStateForSvpClient(); + } catch (Exception e) { + logger.warn("Exception in getStateForSvpClient", e); + throw new VMException("Exception in getStateForSvpClient", e); + } + } + public byte[] getStateForDebugging(Object[] args) throws VMException { logger.trace("getStateForDebugging"); @@ -812,9 +839,15 @@ public byte[] getFederatorPublicKeyOfType(Object[] args) throws VMException { public Long getFederationCreationTime(Object[] args) { logger.trace("getFederationCreationTime"); + Instant activeFederationCreationTime = bridgeSupport.getActiveFederationCreationTime(); - // Return the creation time in milliseconds from the epoch - return bridgeSupport.getActiveFederationCreationTime().toEpochMilli(); + if (!activations.isActive(ConsensusRule.RSKIP419)) { + // Return the creation time in milliseconds from the epoch + return activeFederationCreationTime.toEpochMilli(); + } + + // Return the creation time in seconds from the epoch + return activeFederationCreationTime.getEpochSecond(); } public long getFederationCreationBlockNumber(Object[] args) { @@ -887,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) { @@ -1027,6 +1065,125 @@ public byte[] getPendingFederatorPublicKeyOfType(Object[] args) throws VMExcepti return publicKey; } + /** + * Retrieves the proposed federation Bitcoin address as a Base58 string. + * + *

+ * This method attempts to fetch the address of the proposed federation. If the + * proposed federation is present, it converts the address to its Base58 representation. + * If not, an empty string is returned. + *

+ * + * @param args Additional arguments (currently unused) + * @return The Base58 encoded Bitcoin address of the proposed federation, or an empty + * string if no proposed federation is present. + */ + public String getProposedFederationAddress(Object[] args) { + logger.trace("getProposedFederationAddress"); + + return bridgeSupport.getProposedFederationAddress() + .map(Address::toBase58) + .orElse(""); + } + + /** + * Retrieves the size of the proposed federation, if it exists. + * + *

+ * This method returns the number of members in the proposed federation. If no proposed federation exists, + * it returns a default response code {@link FederationChangeResponseCode#FEDERATION_NON_EXISTENT} that indicates + * the federation does not exist. + *

+ * + * @param args unused arguments for this method (can be null or empty). + * @return the size of the proposed federation (number of members), or the default code from + * {@link FederationChangeResponseCode#FEDERATION_NON_EXISTENT} if no proposed federation is available. + */ + public int getProposedFederationSize(Object[] args) { + logger.trace("getProposedFederationSize"); + + return bridgeSupport.getProposedFederationSize() + .orElse(FederationChangeResponseCode.FEDERATION_NON_EXISTENT.getCode()); + } + + /** + * Retrieves the creation time of the proposed federation in seconds since the epoch. + * + *

+ * This method checks if a proposed federation exists and returns its creation time in + * seconds since the Unix epoch. If no proposed federation exists, it returns -1. + *

+ * + * @param args unused arguments for this method (can be null or empty). + * @return the creation time of the proposed federation in seconds since the epoch, + * or -1 if no proposed federation exists. + */ + public Long getProposedFederationCreationTime(Object[] args) { + logger.trace("getProposedFederationCreationTime"); + + return bridgeSupport.getProposedFederationCreationTime() + .map(Instant::getEpochSecond) + .orElse(-1L); + } + + /** + * Retrieves the block number of the proposed federation's creation. + * + *

+ * This method checks if a proposed federation exists and returns the block number at which it was created. + * If no proposed federation exists, it returns the default code defined in + * {@link FederationChangeResponseCode#FEDERATION_NON_EXISTENT}. + *

+ * + * @param args unused arguments for this method (can be null or empty). + * @return the block number of the proposed federation's creation, or + * the code from {@link FederationChangeResponseCode#FEDERATION_NON_EXISTENT} + * if no proposed federation exists. + */ + public long getProposedFederationCreationBlockNumber(Object[] args) { + logger.trace("getProposedFederationCreationBlockNumber"); + + return bridgeSupport.getProposedFederationCreationBlockNumber() + .orElse((long) FederationChangeResponseCode.FEDERATION_NON_EXISTENT.getCode()); + } + + /** + * Retrieves the public key of the proposed federator at the specified index and key type. + * + *

+ * This method extracts the index and key type from the provided arguments, retrieves the + * public key of the proposed federator, and returns it. If no public key is found, an empty byte + * array is returned. + *

+ * + *

+ * The first argument in the {@code args} array is expected to be a {@link BigInteger} representing + * the federator's index. The second argument is expected to be a {@link String} representing + * the key type, which is converted into a {@link FederationMember.KeyType}. + *

+ * + * @param args an array of arguments, where {@code args[0]} is a {@link BigInteger} for the federator's index, + * and {@code args[1]} is a {@link String} for the key type. + * @return a byte array containing the federator's public key, or an empty byte array if not found. + * @throws VMException if an error occurs while processing the key type. + */ + public byte[] getProposedFederatorPublicKeyOfType(Object[] args) throws VMException { + logger.trace("getProposedFederatorPublicKeyOfType"); + + int index = ((BigInteger) args[0]).intValue(); + + FederationMember.KeyType keyType; + try { + keyType = FederationMember.KeyType.byValue((String) args[1]); + } catch (Exception e) { + logger.warn("Exception in getProposedFederatorPublicKeyOfType", e); + throw new VMException("Exception in getProposedFederatorPublicKeyOfType", e); + } + + return bridgeSupport.getProposedFederatorPublicKeyOfType(index, keyType) + .orElse(new byte[]{}); + } + public Integer getLockWhitelistSize(Object[] args) { logger.trace("getLockWhitelistSize"); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java b/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java index 9202aebebfe..618c88d5655 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java @@ -4,7 +4,6 @@ import org.ethereum.solidity.SolidityType; public enum BridgeEvents { - LOCK_BTC("lock_btc", new CallTransaction.Param[] { new CallTransaction.Param(true, Fields.RECEIVER, SolidityType.getType(SolidityType.ADDRESS)), new CallTransaction.Param(false, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), @@ -44,6 +43,10 @@ public enum BridgeEvents { new CallTransaction.Param(false, "newFederationBtcAddress", SolidityType.getType(SolidityType.STRING)), new CallTransaction.Param(false, "activationHeight", SolidityType.getType(SolidityType.INT256)) }), + COMMIT_FEDERATION_FAILED("commit_federation_failed", new CallTransaction.Param[] { + new CallTransaction.Param(false, "proposedFederationRedeemScript", SolidityType.getType(SolidityType.BYTES)), + new CallTransaction.Param(false, "blockNumber", SolidityType.getType(SolidityType.INT256)) + }), RELEASE_REQUESTED("release_requested", new CallTransaction.Param[] { new CallTransaction.Param(true, "rskTxHash", SolidityType.getType(SolidityType.BYTES32)), new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), @@ -90,14 +93,14 @@ public CallTransaction.Function getEvent() { } private static class Fields { - private static final String RECEIVER = "receiver"; - private static final String SENDER = "sender"; private static final String AMOUNT = "amount"; - private static final String REASON = "reason"; + private static final String BTC_DESTINATION_ADDRESS = "btcDestinationAddress"; private static final String BTC_TX_HASH = "btcTxHash"; + private static final String REASON = "reason"; + private static final String RECEIVER = "receiver"; private static final String RELEASE_RSK_TX_HASH = "releaseRskTxHash"; private static final String RELEASE_RSK_TX_HASHES = "releaseRskTxHashes"; + private static final String SENDER = "sender"; private static final String UTXO_OUTPOINT_VALUES = "utxoOutpointValues"; - private static final String BTC_DESTINATION_ADDRESS = "btcDestinationAddress"; } } diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java b/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java index 80458a14139..62a9881f1bb 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java @@ -353,9 +353,9 @@ public enum BridgeMethods { ), GET_PENDING_FEDERATOR_PUBLIC_KEY( CallTransaction.Function.fromSignature( - "getPendingFederatorPublicKey", - new String[]{"int256"}, - new String[]{"bytes"} + "getPendingFederatorPublicKey", + new String[]{"int256"}, + new String[]{"bytes"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKey, @@ -365,9 +365,9 @@ public enum BridgeMethods { ), GET_PENDING_FEDERATOR_PUBLIC_KEY_OF_TYPE( CallTransaction.Function.fromSignature( - "getPendingFederatorPublicKeyOfType", - new String[]{"int256", "string"}, - new String[]{"bytes"} + "getPendingFederatorPublicKeyOfType", + new String[]{"int256", "string"}, + new String[]{"bytes"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKeyOfType, @@ -377,9 +377,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATION_ADDRESS( CallTransaction.Function.fromSignature( - "getRetiringFederationAddress", - new String[]{}, - new String[]{"string"} + "getRetiringFederationAddress", + new String[]{}, + new String[]{"string"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederationAddress, @@ -388,9 +388,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATION_CREATION_BLOCK_NUMBER( CallTransaction.Function.fromSignature( - "getRetiringFederationCreationBlockNumber", - new String[]{}, - new String[]{"int256"} + "getRetiringFederationCreationBlockNumber", + new String[]{}, + new String[]{"int256"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationBlockNumber, @@ -399,9 +399,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATION_CREATION_TIME( CallTransaction.Function.fromSignature( - "getRetiringFederationCreationTime", - new String[]{}, - new String[]{"int256"} + "getRetiringFederationCreationTime", + new String[]{}, + new String[]{"int256"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationTime, @@ -410,9 +410,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATION_SIZE( CallTransaction.Function.fromSignature( - "getRetiringFederationSize", - new String[]{}, - new String[]{"int256"} + "getRetiringFederationSize", + new String[]{}, + new String[]{"int256"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederationSize, @@ -421,9 +421,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATION_THRESHOLD( CallTransaction.Function.fromSignature( - "getRetiringFederationThreshold", - new String[]{}, - new String[]{"int256"} + "getRetiringFederationThreshold", + new String[]{}, + new String[]{"int256"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederationThreshold, @@ -432,9 +432,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATOR_PUBLIC_KEY( CallTransaction.Function.fromSignature( - "getRetiringFederatorPublicKey", - new String[]{"int256"}, - new String[]{"bytes"} + "getRetiringFederatorPublicKey", + new String[]{"int256"}, + new String[]{"bytes"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKey, @@ -444,9 +444,9 @@ public enum BridgeMethods { ), GET_RETIRING_FEDERATOR_PUBLIC_KEY_OF_TYPE( CallTransaction.Function.fromSignature( - "getRetiringFederatorPublicKeyOfType", - new String[]{"int256", "string"}, - new String[]{"bytes"} + "getRetiringFederatorPublicKeyOfType", + new String[]{"int256", "string"}, + new String[]{"bytes"} ), fixedCost(3000L), (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKeyOfType, @@ -454,22 +454,94 @@ public enum BridgeMethods { fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), - GET_STATE_FOR_BTC_RELEASE_CLIENT( + GET_PROPOSED_FEDERATION_ADDRESS( CallTransaction.Function.fromSignature( - "getStateForBtcReleaseClient", + "getProposedFederationAddress", + new String[]{}, + new String[]{ "string" } + ), + fixedCost(3000L), + (BridgeMethodExecutorTyped) Bridge::getProposedFederationAddress, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), + GET_PROPOSED_FEDERATION_SIZE( + CallTransaction.Function.fromSignature( + "getProposedFederationSize", new String[]{}, - new String[]{"bytes"} + new String[]{ "int256" } + ), + fixedCost(3000L), + (BridgeMethodExecutorTyped) Bridge::getProposedFederationSize, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), + GET_PROPOSED_FEDERATION_CREATION_TIME( + CallTransaction.Function.fromSignature( + "getProposedFederationCreationTime", + new String[]{}, + new String[]{ "int256" } + ), + fixedCost(3000L), + (BridgeMethodExecutorTyped) Bridge::getProposedFederationCreationTime, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), + GET_PROPOSED_FEDERATION_CREATION_BLOCK_NUMBER( + CallTransaction.Function.fromSignature( + "getProposedFederationCreationBlockNumber", + new String[]{}, + new String[]{ "int256" } + ), + fixedCost(3000L), + (BridgeMethodExecutorTyped) Bridge::getProposedFederationCreationBlockNumber, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), + GET_PROPOSED_FEDERATOR_PUBLIC_KEY_OF_TYPE( + CallTransaction.Function.fromSignature( + "getProposedFederatorPublicKeyOfType", + new String[]{ "int256", "string" }, + new String[]{ "bytes" } + ), + fixedCost(3000L), + (BridgeMethodExecutorTyped) Bridge::getProposedFederatorPublicKeyOfType, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), + GET_STATE_FOR_BTC_RELEASE_CLIENT( + CallTransaction.Function.fromSignature( + "getStateForBtcReleaseClient", + new String[]{}, + new String[]{"bytes"} ), fixedCost(4000L), (BridgeMethodExecutorTyped) Bridge::getStateForBtcReleaseClient, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), + GET_STATE_FOR_SVP_CLIENT( + CallTransaction.Function.fromSignature( + "getStateForSvpClient", + new String[]{}, + new String[]{"bytes"} + ), + fixedCost(4000L), // TODO: check fixed cost value + (BridgeMethodExecutorTyped) Bridge::getStateForSvpClient, + activations -> activations.isActive(RSKIP419), + fixedPermission(true), + CallTypeHelper.ALLOW_STATIC_CALL + ), GET_STATE_FOR_DEBUGGING( CallTransaction.Function.fromSignature( - "getStateForDebugging", - new String[]{}, - new String[]{"bytes"} + "getStateForDebugging", + new String[]{}, + new String[]{"bytes"} ), fixedCost(3_000_000L), (BridgeMethodExecutorTyped) Bridge::getStateForDebugging, @@ -478,9 +550,9 @@ public enum BridgeMethods { ), GET_LOCKING_CAP( CallTransaction.Function.fromSignature( - "getLockingCap", - new String[]{}, - new String[]{"int256"} + "getLockingCap", + new String[]{}, + new String[]{"int256"} ), fixedCost(3_000L), (BridgeMethodExecutorTyped) Bridge::getLockingCap, @@ -490,9 +562,9 @@ public enum BridgeMethods { ), GET_ACTIVE_POWPEG_REDEEM_SCRIPT( CallTransaction.Function.fromSignature( - "getActivePowpegRedeemScript", - new String[]{}, - new String[]{"bytes"} + "getActivePowpegRedeemScript", + new String[]{}, + new String[]{"bytes"} ), fixedCost(30_000L), (BridgeMethodExecutorTyped) Bridge::getActivePowpegRedeemScript, @@ -502,9 +574,9 @@ public enum BridgeMethods { ), GET_ACTIVE_FEDERATION_CREATION_BLOCK_HEIGHT( CallTransaction.Function.fromSignature( - "getActiveFederationCreationBlockHeight", - new String[]{}, - new String[]{"uint256"} + "getActiveFederationCreationBlockHeight", + new String[]{}, + new String[]{"uint256"} ), fixedCost(3_000L), (BridgeMethodExecutorTyped) Bridge::getActiveFederationCreationBlockHeight, @@ -514,9 +586,9 @@ public enum BridgeMethods { ), INCREASE_LOCKING_CAP( CallTransaction.Function.fromSignature( - "increaseLockingCap", - new String[]{"int256"}, - new String[]{"bool"} + "increaseLockingCap", + new String[]{"int256"}, + new String[]{"bool"} ), fixedCost(8_000L), (BridgeMethodExecutorTyped) Bridge::increaseLockingCap, @@ -525,9 +597,9 @@ public enum BridgeMethods { ), IS_BTC_TX_HASH_ALREADY_PROCESSED( CallTransaction.Function.fromSignature( - "isBtcTxHashAlreadyProcessed", - new String[]{"string"}, - new String[]{"bool"} + "isBtcTxHashAlreadyProcessed", + new String[]{"string"}, + new String[]{"bool"} ), fixedCost(23000L), (BridgeMethodExecutorTyped) Bridge::isBtcTxHashAlreadyProcessed, @@ -536,9 +608,9 @@ public enum BridgeMethods { ), RECEIVE_HEADERS( CallTransaction.Function.fromSignature( - "receiveHeaders", - new String[]{"bytes[]"}, - new String[]{} + "receiveHeaders", + new String[]{"bytes[]"}, + new String[]{} ), fromMethod(Bridge::receiveHeadersGetCost), Bridge.executeIfElse( 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 18a4b6bfee8..b3f5c252eb9 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSerializationUtils.java @@ -18,6 +18,9 @@ package co.rsk.peg; +import static co.rsk.peg.federation.FederationFormatVersion.*; +import static java.util.Objects.isNull; + import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.script.Script; import co.rsk.core.RskAddress; @@ -37,7 +40,6 @@ import org.ethereum.util.RLP; import org.ethereum.util.RLPElement; import org.ethereum.util.RLPList; - import javax.annotation.Nullable; import java.io.*; import java.math.BigInteger; @@ -46,12 +48,11 @@ import java.util.*; import java.util.stream.Collectors; -import static co.rsk.peg.federation.FederationFormatVersion.*; - /** * Created by mario on 20/04/17. */ public class BridgeSerializationUtils { + private static final int FEDERATION_RLP_LIST_SIZE = 3; private static final int FEDERATION_CREATION_TIME_INDEX = 0; private static final int FEDERATION_CREATION_BLOCK_NUMBER_INDEX = 1; @@ -61,45 +62,147 @@ private BridgeSerializationUtils() { throw new IllegalAccessError("Utility class, do not instantiate it"); } - public static byte[] serializeMap(SortedMap map) { - int ntxs = map.size(); + private static byte[] serializeRskTxHash(Keccak256 rskTxHash) { + return RLP.encodeElement(rskTxHash.getBytes()); + } + + public static Keccak256 deserializeRskTxHash(byte[] rskTxHashSerialized) { + if (isNull(rskTxHashSerialized)) { + throw new IllegalArgumentException("Serialized hash cannot be null."); + } + return new Keccak256(rskTxHashSerialized); + } + + public static byte[] serializeBtcTransaction(BtcTransaction btcTransaction) { + return RLP.encodeElement(btcTransaction.bitcoinSerialize()); + } + + public static BtcTransaction deserializeBtcTransactionWithInputs(byte[] serializedTx, NetworkParameters networkParameters) { + return deserializeBtcTransaction(serializedTx, networkParameters, true); + } + + public static BtcTransaction deserializeBtcTransactionWithoutInputs(byte[] serializedTx, NetworkParameters networkParameters) { + return deserializeBtcTransaction(serializedTx, networkParameters, false); + } + + private static BtcTransaction deserializeBtcTransaction( + byte[] serializedTx, + NetworkParameters networkParameters, + boolean txHasInputs) { + + if (serializedTx == null || serializedTx.length == 0) { + return null; + } + + RLPElement rawTxElement = RLP.decode2(serializedTx).get(0); + byte[] rawTx = rawTxElement.getRLPData(); + + return deserializeBtcTransactionFromRawTx(rawTx, networkParameters, txHasInputs); + } + + private static BtcTransaction deserializeBtcTransactionWithInputsFromRawTx(byte[] rawTx, NetworkParameters networkParameters) { + return deserializeBtcTransactionFromRawTx(rawTx, networkParameters, true); + } + + private static BtcTransaction deserializeBtcTransactionFromRawTx( + byte[] rawTx, + NetworkParameters networkParameters, + boolean txHasInputs) { + + if (!txHasInputs) { + BtcTransaction tx = new BtcTransaction(networkParameters); + tx.parseNoInputs(rawTx); + return tx; + } + + return new BtcTransaction(networkParameters, rawTx); + } + + public static byte[] serializeRskTxWaitingForSignatures( + Map.Entry rskTxWaitingForSignaturesEntry) { + if (rskTxWaitingForSignaturesEntry == null) { + return RLP.encodedEmptyList(); + } + + byte[][] serializedRskTxWaitingForSignaturesEntry = + serializeRskTxWaitingForSignaturesEntry(rskTxWaitingForSignaturesEntry); + return RLP.encodeList(serializedRskTxWaitingForSignaturesEntry); + } + + public static byte[] serializeRskTxsWaitingForSignatures( + SortedMap rskTxWaitingForSignaturesMap) { + + int numberOfRskTxsWaitingForSignatures = rskTxWaitingForSignaturesMap.size(); + byte[][] serializedRskTxWaitingForSignaturesMap = new byte[numberOfRskTxsWaitingForSignatures * 2][]; - byte[][] bytes = new byte[ntxs * 2][]; int n = 0; + for (Map.Entry rskTxWaitingForSignaturesEntry : rskTxWaitingForSignaturesMap.entrySet()) { + byte[][] serializedRskTxWaitingForSignaturesEntry = serializeRskTxWaitingForSignaturesEntry(rskTxWaitingForSignaturesEntry); + serializedRskTxWaitingForSignaturesMap[n++] = serializedRskTxWaitingForSignaturesEntry[0]; + serializedRskTxWaitingForSignaturesMap[n++] = serializedRskTxWaitingForSignaturesEntry[1]; + } + + return RLP.encodeList(serializedRskTxWaitingForSignaturesMap); + } + + private static byte[][] serializeRskTxWaitingForSignaturesEntry( + Map.Entry rskTxWaitingForSignaturesEntry) { - for (Map.Entry entry : map.entrySet()) { - bytes[n++] = RLP.encodeElement(entry.getKey().getBytes()); - bytes[n++] = RLP.encodeElement(entry.getValue().bitcoinSerialize()); + byte[] serializedRskTxWaitingForSignaturesEntryKey = + serializeRskTxHash(rskTxWaitingForSignaturesEntry.getKey()); + byte[] serializedRskTxWaitingForSignaturesEntryValue = + serializeBtcTransaction(rskTxWaitingForSignaturesEntry.getValue()); + + return new byte[][] { serializedRskTxWaitingForSignaturesEntryKey, serializedRskTxWaitingForSignaturesEntryValue }; + } + + public static Map.Entry deserializeRskTxWaitingForSignatures( + byte[] data, NetworkParameters networkParameters) { + if (data == null || data.length == 0) { + return null; } - return RLP.encodeList(bytes); + RLPList rlpList = (RLPList) RLP.decode2(data).get(0); + return deserializeRskTxWaitingForSignaturesEntry(rlpList, 0, networkParameters); } - public static SortedMap deserializeMap(byte[] data, NetworkParameters networkParameters, boolean noInputsTxs) { - SortedMap map = new TreeMap<>(); + public static SortedMap deserializeRskTxsWaitingForSignatures( + byte[] data, NetworkParameters networkParameters) { + + SortedMap rskTxsWaitingForSignaturesMap = new TreeMap<>(); if (data == null || data.length == 0) { - return map; + return rskTxsWaitingForSignaturesMap; } - RLPList rlpList = (RLPList)RLP.decode2(data).get(0); + RLPList rlpList = (RLPList) RLP.decode2(data).get(0); + int numberOfRskTxsWaitingForSignatures = rlpList.size() / 2; - int ntxs = rlpList.size() / 2; - - for (int k = 0; k < ntxs; k++) { - Keccak256 hash = new Keccak256(rlpList.get(k * 2).getRLPData()); - byte[] payload = rlpList.get(k * 2 + 1).getRLPData(); - BtcTransaction tx; - if (!noInputsTxs) { - tx = new BtcTransaction(networkParameters, payload); - } else { - tx = new BtcTransaction(networkParameters); - tx.parseNoInputs(payload); - } - map.put(hash, tx); + for (int k = 0; k < numberOfRskTxsWaitingForSignatures; k++) { + Map.Entry rskTxWaitingForSignaturesEntry = + deserializeRskTxWaitingForSignaturesEntry(rlpList, k, networkParameters); + + rskTxsWaitingForSignaturesMap.put(rskTxWaitingForSignaturesEntry.getKey(), rskTxWaitingForSignaturesEntry.getValue()); } - return map; + return rskTxsWaitingForSignaturesMap; + } + + 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(); + Keccak256 rskTxHash = deserializeRskTxHash(rskTxHashData); + + RLPElement btcTxRLPElement = rlpList.get(index * 2 + 1); + byte[] btcRawTx = btcTxRLPElement.getRLPData(); + BtcTransaction btcTx = deserializeBtcTransactionWithInputsFromRawTx(btcRawTx, networkParameters); + + return new AbstractMap.SimpleEntry<>(rskTxHash, btcTx); } public static byte[] serializeUTXOList(List list) { @@ -145,37 +248,6 @@ public static List deserializeUTXOList(byte[] data) { return list; } - public static byte[] serializeSet(SortedSet set) { - int nhashes = set.size(); - - byte[][] bytes = new byte[nhashes][]; - int n = 0; - - for (Sha256Hash hash : set) { - bytes[n++] = RLP.encodeElement(hash.getBytes()); - } - - return RLP.encodeList(bytes); - } - - public static SortedSet deserializeSet(byte[] data) { - SortedSet set = new TreeSet<>(); - - if (data == null || data.length == 0) { - return set; - } - - RLPList rlpList = (RLPList)RLP.decode2(data).get(0); - - int nhashes = rlpList.size(); - - for (int k = 0; k < nhashes; k++) { - set.add(Sha256Hash.wrap(rlpList.get(k).getRLPData())); - } - - return set; - } - public static byte[] serializeMapOfHashesToLong(Map map) { byte[][] bytes = new byte[map.size() * 2][]; int n = 0; @@ -604,7 +676,8 @@ private static List deserializeReleaseRequestQueueWit byte[] addressBytes = rlpList.get(k * 3).getRLPData(); Address address = new Address(networkParameters, addressBytes); long amount = BigIntegers.fromUnsignedByteArray(rlpList.get(k * 3 + 1).getRLPData()).longValue(); - Keccak256 txHash = new Keccak256(rlpList.get(k * 3 + 2).getRLPData()); + + Keccak256 txHash = deserializeRskTxHash(rlpList.get(k * 3 + 2).getRLPData()); entries.add(new ReleaseRequestQueue.Entry(address, Coin.valueOf(amount), txHash)); } @@ -628,7 +701,7 @@ public static byte[] serializePegoutsWaitingForConfirmations(PegoutsWaitingForCo int n = 0; for (PegoutsWaitingForConfirmations.Entry entry : entries) { - bytes[n++] = RLP.encodeElement(entry.getBtcTransaction().bitcoinSerialize()); + bytes[n++] = serializeBtcTransaction(entry.getBtcTransaction()); bytes[n++] = RLP.encodeBigInteger(BigInteger.valueOf(entry.getPegoutCreationRskBlockNumber())); } @@ -643,9 +716,9 @@ public static byte[] serializePegoutsWaitingForConfirmationsWithTxHash(PegoutsWa int n = 0; for (PegoutsWaitingForConfirmations.Entry entry : entries) { - bytes[n++] = RLP.encodeElement(entry.getBtcTransaction().bitcoinSerialize()); + bytes[n++] = serializeBtcTransaction(entry.getBtcTransaction()); bytes[n++] = RLP.encodeBigInteger(BigInteger.valueOf(entry.getPegoutCreationRskBlockNumber())); - bytes[n++] = RLP.encodeElement(entry.getPegoutCreationRskTxHash().getBytes()); + bytes[n++] = serializeRskTxHash(entry.getPegoutCreationRskTxHash()); } return RLP.encodeList(bytes); @@ -697,7 +770,7 @@ private static PegoutsWaitingForConfirmations deserializePegoutWaitingForConfirm BtcTransaction tx = new BtcTransaction(networkParameters, txPayload); long height = BigIntegers.fromUnsignedByteArray(rlpList.get(k * 3 + 1).getRLPData()).longValue(); - Keccak256 rskTxHash = new Keccak256(rlpList.get(k * 3 + 2).getRLPData()); + Keccak256 rskTxHash = deserializeRskTxHash(rlpList.get(k * 3 + 2).getRLPData()); entries.add(new PegoutsWaitingForConfirmations.Entry(tx, height, rskTxHash)); } @@ -786,7 +859,7 @@ public static FlyoverFederationInformation deserializeFlyoverFederationInformati if (rlpList.size() != 2) { throw new RuntimeException(String.format("Invalid serialized Fast Bridge Federation: expected 2 value but got %d", rlpList.size())); } - Keccak256 derivationHash = new Keccak256(rlpList.get(0).getRLPData()); + Keccak256 derivationHash = deserializeRskTxHash(rlpList.get(0).getRLPData()); byte[] federationP2SH = rlpList.get(1).getRLPData(); return new FlyoverFederationInformation(derivationHash, federationP2SH, flyoverScriptHash); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeState.java b/rskj-core/src/main/java/co/rsk/peg/BridgeState.java index b1723503f11..e35d396f939 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeState.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeState.java @@ -111,7 +111,7 @@ public Map stateToMap() { public byte[] getEncoded() throws IOException { byte[] rlpBtcBlockchainBestChainHeight = RLP.encodeBigInteger(BigInteger.valueOf(this.btcBlockchainBestChainHeight)); byte[] rlpActiveFederationBtcUTXOs = RLP.encodeElement(BridgeSerializationUtils.serializeUTXOList(activeFederationBtcUTXOs)); - byte[] rlpRskTxsWaitingForSignatures = RLP.encodeElement(BridgeSerializationUtils.serializeMap(rskTxsWaitingForSignatures)); + byte[] rlpRskTxsWaitingForSignatures = RLP.encodeElement(BridgeSerializationUtils.serializeRskTxsWaitingForSignatures(rskTxsWaitingForSignatures)); byte[] serializedReleaseRequestQueue = shouldUsePapyrusEncoding(this.activations) ? BridgeSerializationUtils.serializeReleaseRequestQueueWithTxHash(releaseRequestQueue): BridgeSerializationUtils.serializeReleaseRequestQueue(releaseRequestQueue); @@ -133,7 +133,7 @@ public static BridgeState create(BridgeConstants bridgeConstants, byte[] data, @ byte[] btcUTXOsBytes = rlpList.get(1).getRLPData(); List btcUTXOs = BridgeSerializationUtils.deserializeUTXOList(btcUTXOsBytes); byte[] rskTxsWaitingForSignaturesBytes = rlpList.get(2).getRLPData(); - SortedMap rskTxsWaitingForSignatures = BridgeSerializationUtils.deserializeMap(rskTxsWaitingForSignaturesBytes, bridgeConstants.getBtcParams(), false); + SortedMap rskTxsWaitingForSignatures = BridgeSerializationUtils.deserializeRskTxsWaitingForSignatures(rskTxsWaitingForSignaturesBytes, bridgeConstants.getBtcParams()); byte[] releaseRequestQueueBytes = rlpList.get(3).getRLPData(); ReleaseRequestQueue releaseRequestQueue = new ReleaseRequestQueue(BridgeSerializationUtils.deserializeReleaseRequestQueue(releaseRequestQueueBytes, bridgeConstants.getBtcParams(), shouldUsePapyrusEncoding(activations))); byte[] pegoutsWaitingForConfirmationsBytes = rlpList.get(4).getRLPData(); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageIndexKey.java b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageIndexKey.java index 9c42f5952ed..977495fb5eb 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageIndexKey.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageIndexKey.java @@ -22,6 +22,11 @@ public enum BridgeStorageIndexKey { FAST_BRIDGE_FEDERATION_INFORMATION("fastBridgeFederationInformation"), PEGOUT_TX_SIG_HASH("pegoutTxSigHash"), + + SVP_FUND_TX_HASH_UNSIGNED("svpFundTxHashUnsigned"), + SVP_FUND_TX_SIGNED("svpFundTxSigned"), + SVP_SPEND_TX_HASH_UNSIGNED("svpSpendTxHashUnsigned"), + SVP_SPEND_TX_WAITING_FOR_SIGNATURES("svpSpendTxWaitingForSignatures"), ; private final String key; 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 1b9797bd1c5..27c6a05b34a 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeStorageProvider.java @@ -18,22 +18,23 @@ package co.rsk.peg; +import static co.rsk.peg.BridgeStorageIndexKey.*; +import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*; + import co.rsk.bitcoinj.core.*; import co.rsk.core.RskAddress; import co.rsk.crypto.Keccak256; import co.rsk.peg.bitcoin.CoinbaseInformation; import co.rsk.peg.flyover.FlyoverFederationInformation; +import java.io.IOException; +import java.util.*; 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; -import java.io.IOException; -import java.util.*; -import static co.rsk.peg.BridgeStorageIndexKey.*; - -import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*; - /** * Provides an object oriented facade of the bridge contract memory. * @see co.rsk.peg.BridgeStorageProvider @@ -41,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; @@ -77,6 +79,15 @@ public class BridgeStorageProvider { private Set pegoutTxSigHashes; + private Sha256Hash svpFundTxHashUnsigned; + private boolean isSvpFundTxHashUnsignedSet = false; + private BtcTransaction svpFundTxSigned; + private boolean isSvpFundTxSignedSet = false; + private Sha256Hash svpSpendTxHashUnsigned; + private boolean isSvpSpendTxHashUnsignedSet = false; + private Map.Entry svpSpendTxWaitingForSignatures; + private boolean isSvpSpendTxWaitingForSignaturesSet = false; + public BridgeStorageProvider( Repository repository, RskAddress contractAddress, @@ -209,7 +220,7 @@ public PegoutsWaitingForConfirmations getPegoutsWaitingForConfirmations() throws entries.addAll(getFromRepository( PEGOUTS_WAITING_FOR_CONFIRMATIONS_WITH_TXHASH_KEY, - data -> BridgeSerializationUtils.deserializePegoutsWaitingForConfirmations(data, networkParameters, true).getEntries())); + data -> BridgeSerializationUtils.deserializePegoutsWaitingForConfirmations(data, networkParameters, true).getEntries())); pegoutsWaitingForConfirmations = new PegoutsWaitingForConfirmations(entries); @@ -235,7 +246,7 @@ public SortedMap getPegoutsWaitingForSignatures() thr pegoutsWaitingForSignatures = getFromRepository( PEGOUTS_WAITING_FOR_SIGNATURES, - data -> BridgeSerializationUtils.deserializeMap(data, networkParameters, false) + data -> BridgeSerializationUtils.deserializeRskTxsWaitingForSignatures(data, networkParameters) ); return pegoutsWaitingForSignatures; } @@ -245,7 +256,7 @@ public void savePegoutsWaitingForSignatures() { return; } - safeSaveToRepository(PEGOUTS_WAITING_FOR_SIGNATURES, pegoutsWaitingForSignatures, BridgeSerializationUtils::serializeMap); + safeSaveToRepository(PEGOUTS_WAITING_FOR_SIGNATURES, pegoutsWaitingForSignatures, BridgeSerializationUtils::serializeRskTxsWaitingForSignatures); } public CoinbaseInformation getCoinbaseInformation(Sha256Hash blockHash) { @@ -373,7 +384,7 @@ public Optional getFlyoverFederationInformation(by return Optional.empty(); } - FlyoverFederationInformation flyoverFederationInformationInStorage = this.safeGetFromRepository( + FlyoverFederationInformation flyoverFederationInformationInStorage = safeGetFromRepository( getStorageKeyForFlyoverFederationInformation(flyoverFederationRedeemScriptHash), data -> BridgeSerializationUtils.deserializeFlyoverFederationInformation(data, flyoverFederationRedeemScriptHash) ); @@ -462,12 +473,12 @@ public Optional getNextPegoutHeight() { public void setNextPegoutHeight(long nextPegoutHeight) { this.nextPegoutHeight = nextPegoutHeight; } - + protected void saveNextPegoutHeight() { if (nextPegoutHeight == null || !activations.isActive(RSKIP271)) { return; } - + safeSaveToRepository(NEXT_PEGOUT_HEIGHT_KEY, nextPegoutHeight, BridgeSerializationUtils::serializeLong); } @@ -520,6 +531,187 @@ protected void savePegoutTxSigHashes() { )); } + public Optional getSvpFundTxHashUnsigned() { + if (!activations.isActive(RSKIP419)) { + return Optional.empty(); + } + + if (svpFundTxHashUnsigned != null) { + return Optional.of(svpFundTxHashUnsigned); + } + + // Return empty if the svp fund tx hash unsigned was explicitly set to null + if (isSvpFundTxHashUnsignedSet) { + return Optional.empty(); + } + + svpFundTxHashUnsigned = safeGetFromRepository( + SVP_FUND_TX_HASH_UNSIGNED.getKey(), BridgeSerializationUtils::deserializeSha256Hash); + return Optional.ofNullable(svpFundTxHashUnsigned); + } + + public Optional getSvpFundTxSigned() { + if (!activations.isActive(RSKIP419)) { + return Optional.empty(); + } + + if (svpFundTxSigned != null) { + return Optional.of(svpFundTxSigned); + } + + // Return empty if the svp fund tx signed was explicitly set to null + if (isSvpFundTxSignedSet) { + return Optional.empty(); + } + + svpFundTxSigned = safeGetFromRepository(SVP_FUND_TX_SIGNED, + data -> BridgeSerializationUtils.deserializeBtcTransactionWithInputs(data, networkParameters)); + return Optional.ofNullable(svpFundTxSigned); + } + + public Optional getSvpSpendTxHashUnsigned() { + if (!activations.isActive(RSKIP419)) { + return Optional.empty(); + } + + if (svpSpendTxHashUnsigned != null) { + return Optional.of(svpSpendTxHashUnsigned); + } + + // Return empty if the svp spend tx hash unsigned was explicitly set to null + if (isSvpSpendTxHashUnsignedSet) { + return Optional.empty(); + } + + svpSpendTxHashUnsigned = safeGetFromRepository( + SVP_SPEND_TX_HASH_UNSIGNED.getKey(), BridgeSerializationUtils::deserializeSha256Hash); + return Optional.ofNullable(svpSpendTxHashUnsigned); + } + + public Optional> getSvpSpendTxWaitingForSignatures() { + if (!activations.isActive(RSKIP419)) { + return Optional.empty(); + } + + if (svpSpendTxWaitingForSignatures != null) { + return Optional.of(svpSpendTxWaitingForSignatures); + } + + // Return empty if the svp spend tx waiting for signatures was explicitly set to null + if (isSvpSpendTxWaitingForSignaturesSet) { + return Optional.empty(); + } + + svpSpendTxWaitingForSignatures = safeGetFromRepository( + SVP_SPEND_TX_WAITING_FOR_SIGNATURES.getKey(), + data -> BridgeSerializationUtils.deserializeRskTxWaitingForSignatures(data, networkParameters)); + + return Optional.ofNullable(svpSpendTxWaitingForSignatures); + } + + public void setSvpFundTxHashUnsigned(Sha256Hash hash) { + this.svpFundTxHashUnsigned = 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; + } + + safeSaveToRepository( + SVP_FUND_TX_HASH_UNSIGNED, + svpFundTxHashUnsigned, + BridgeSerializationUtils::serializeSha256Hash + ); + } + + public void setSvpFundTxSigned(BtcTransaction svpFundTxSigned) { + this.svpFundTxSigned = 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; + } + + safeSaveToRepository( + SVP_FUND_TX_SIGNED, + svpFundTxSigned, + BridgeSerializationUtils::serializeBtcTransaction); + } + + public void setSvpSpendTxHashUnsigned(Sha256Hash hash) { + this.svpSpendTxHashUnsigned = 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; + } + + safeSaveToRepository( + SVP_SPEND_TX_HASH_UNSIGNED, + svpSpendTxHashUnsigned, + BridgeSerializationUtils::serializeSha256Hash); + } + + public void setSvpSpendTxWaitingForSignatures(Map.Entry svpSpendTxWaitingForSignatures) { + boolean hasNullKeyOrValue = svpSpendTxWaitingForSignatures != null && + (svpSpendTxWaitingForSignatures.getKey() == null || svpSpendTxWaitingForSignatures.getValue() == null); + if (hasNullKeyOrValue) { + throw new IllegalArgumentException( + String.format("Invalid svpSpendTxWaitingForSignatures, has null key or value: %s", svpSpendTxWaitingForSignatures) + ); + } + + this.svpSpendTxWaitingForSignatures = svpSpendTxWaitingForSignatures; + this.isSvpSpendTxWaitingForSignaturesSet = true; + } + + public void clearSvpSpendTxWaitingForSignatures() { + logger.info("[clearSvpSpendTxWaitingForSignatures] Clearing spend tx waiting for signatures."); + setSvpSpendTxWaitingForSignatures(null); + } + + private void saveSvpSpendTxWaitingForSignatures() { + if (!activations.isActive(RSKIP419) || !isSvpSpendTxWaitingForSignaturesSet) { + return; + } + + safeSaveToRepository( + SVP_SPEND_TX_WAITING_FOR_SIGNATURES, + svpSpendTxWaitingForSignatures, + BridgeSerializationUtils::serializeRskTxWaitingForSignatures + ); + } + + public void clearSvpValues() { + logger.info("[clearSvpValues] Clearing all SVP values."); + + clearSvpFundTxHashUnsigned(); + clearSvpFundTxSigned(); + clearSvpSpendTxWaitingForSignatures(); + clearSvpSpendTxHashUnsigned(); + } + public void save() { saveBtcTxHashesAlreadyProcessed(); @@ -542,6 +734,11 @@ public void save() { saveNextPegoutHeight(); savePegoutTxSigHashes(); + + saveSvpFundTxHashUnsigned(); + saveSvpFundTxSigned(); + saveSvpSpendTxHashUnsigned(); + saveSvpSpendTxWaitingForSignatures(); } private DataWord getStorageKeyForBtcTxHashAlreadyProcessed(Sha256Hash btcTxHash) { 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 92c93eb4de9..ae5533847ad 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java @@ -17,11 +17,14 @@ */ package co.rsk.peg; +import static co.rsk.peg.BridgeUtils.calculatePegoutTxSize; import static co.rsk.peg.BridgeUtils.getRegularPegoutTxSize; +import static co.rsk.peg.PegUtils.*; import static co.rsk.peg.ReleaseTransactionBuilder.BTC_TX_VERSION_2; -import static co.rsk.peg.bitcoin.BitcoinUtils.findWitnessCommitment; +import static co.rsk.peg.bitcoin.BitcoinUtils.*; import static co.rsk.peg.bitcoin.UtxoUtils.extractOutpointValues; import static co.rsk.peg.pegin.RejectedPeginReason.INVALID_AMOUNT; +import static java.util.Objects.isNull; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*; import co.rsk.bitcoinj.core.*; @@ -31,11 +34,11 @@ import co.rsk.bitcoinj.wallet.SendRequest; import co.rsk.bitcoinj.wallet.Wallet; import co.rsk.core.types.bytes.Bytes; +import co.rsk.peg.bitcoin.*; import co.rsk.peg.constants.BridgeConstants; import co.rsk.core.RskAddress; import co.rsk.crypto.Keccak256; import co.rsk.panic.PanicProcessor; -import co.rsk.peg.bitcoin.*; import co.rsk.peg.btcLockSender.BtcLockSender.TxSenderAddressType; import co.rsk.peg.btcLockSender.BtcLockSenderProvider; import co.rsk.peg.federation.*; @@ -58,6 +61,7 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; +import java.security.SignatureException; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -69,6 +73,7 @@ import org.ethereum.core.*; import org.ethereum.crypto.HashUtil; import org.ethereum.util.ByteUtil; +import org.ethereum.util.RLP; import org.ethereum.vm.DataWord; import org.ethereum.vm.PrecompiledContracts; import org.ethereum.vm.exception.VMException; @@ -335,7 +340,13 @@ private Wallet getRetiringFederationWallet(boolean shouldConsiderFlyoverUTXOs, i * */ public Wallet getUTXOBasedWalletForLiveFederations(List utxos, boolean isFlyoverCompatible) { - return BridgeUtils.getFederationsSpendWallet(btcContext, getLiveFederations(), utxos, isFlyoverCompatible, provider); + return BridgeUtils.getFederationsSpendWallet( + btcContext, + federationSupport.getLiveFederations(), + utxos, + isFlyoverCompatible, + provider + ); } /** @@ -344,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 + ); } /** @@ -388,30 +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); - 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( @@ -423,8 +431,34 @@ public void registerBtcTransaction( } } - private Script getLastRetiredFederationP2SHScript() { - return federationSupport.getLastRetiredFederationP2SHScript().orElse(null); + 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 void registerSvpSpendTx(BtcTransaction btcTx) throws IOException { + registerNewUtxos(btcTx); + provider.clearSvpSpendTxHashUnsigned(); + + logger.info("[registerSvpSpendTx] Going to commit the proposed federation."); + federationSupport.commitProposedFederation(); + } + + private void updateSvpFundTransactionValues(BtcTransaction 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.clearSvpFundTxHashUnsigned(); } @VisibleForTesting @@ -432,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, @@ -451,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, @@ -472,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); @@ -516,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 @@ -524,7 +557,7 @@ private void handleUnprocessableBtcTx( * @deprecated */ @Deprecated - private void legacyProcessPegin( + private void legacyRegisterPegin( BtcTransaction btcTx, Keccak256 rskTxHash, int height @@ -554,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); @@ -570,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( @@ -610,6 +641,7 @@ private void processPegInVersionLegacy( } generateRejectionRelease(btcTx, senderBtcAddress, rskTxHash, totalAmount); + markTxAsProcessed(btcTx); } } @@ -634,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(); @@ -662,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( @@ -692,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, @@ -761,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( @@ -780,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); @@ -797,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)); } /** @@ -969,7 +1002,7 @@ private void requestRelease(Address destinationAddress, co.rsk.core.Coin release public void updateCollections(Transaction rskTx) throws IOException { Context.propagate(btcContext); - eventLogger.logUpdateCollections(rskTx); + logUpdateCollections(rskTx); processFundsMigration(rskTx); @@ -978,6 +1011,203 @@ public void updateCollections(Transaction rskTx) throws IOException { processConfirmedPegouts(rskTx); updateFederationCreationBlockHeights(); + + updateSvpState(rskTx); + } + + private void logUpdateCollections(Transaction rskTx) { + RskAddress sender = rskTx.getSender(signatureCache); + eventLogger.logUpdateCollections(sender); + } + + 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 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 svpFundTxHashUnsigned.isEmpty() + && svpFundTxSigned.isEmpty() + && svpSpendTxHashUnsigned.isEmpty(); + } + + 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(); + } + + private void allowFederationElectionAgain() { + federationSupport.clearProposedFederation(); + provider.clearSvpValues(); + } + + private boolean isSvpOngoing() { + return federationSupport + .getProposedFederation() + .map(proposedFederation -> rskExecutionBlock.getNumber() < proposedFederation.getCreationBlockNumber() + + bridgeConstants.getFederationConstants().getValidationPeriodDurationInBlocks() + ) + .orElse(false); + } + + 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(svpFundTxOutputsValue, proposedFederation.getAddress()); + Address proposedFederationWithFlyoverPrefixAddress = + getFlyoverAddress(networkParameters, bridgeConstants.getProposedFederationFlyoverPrefix(), proposedFederation.getRedeemScript()); + svpFundTransaction.addOutput(svpFundTxOutputsValue, proposedFederationWithFlyoverPrefixAddress); + + // complete tx with input and change output + SendRequest sendRequest = createSvpFundTransactionSendRequest(svpFundTransaction); + activeFederationWallet.completeTx(sendRequest); + + return svpFundTransaction; + } + + private SendRequest createSvpFundTransactionSendRequest(BtcTransaction transaction) { + SendRequest sendRequest = SendRequest.forTx(transaction); + sendRequest.changeAddress = getActiveFederationAddress(); + sendRequest.feePerKb = feePerKbSupport.getFeePerKb(); + sendRequest.missingSigsMode = Wallet.MissingSigsMode.USE_OP_ZERO; + sendRequest.recipientsPayFees = false; + sendRequest.shuffleOutputs = false; + + return sendRequest; + } + + 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); + } + + private BtcTransaction createSvpSpendTransaction(BtcTransaction svpFundTxSigned, Federation proposedFederation) throws IllegalStateException { + BtcTransaction svpSpendTransaction = new BtcTransaction(networkParameters); + svpSpendTransaction.setVersion(BTC_TX_VERSION_2); + + 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( + valueToSend, + federationSupport.getActiveFederationAddress() + ); + + return svpSpendTransaction; + } + + private Coin calculateSvpSpendTxFees(Federation proposedFederation) { + int svpSpendTransactionSize = calculatePegoutTxSize(activations, proposedFederation, 2, 1); + long svpSpendTransactionBackedUpSize = svpSpendTransactionSize * 12L / 10L; // just to be sure the fees sent will be enough + + return feePerKbSupport.getFeePerKb() + .multiply(svpSpendTransactionBackedUpSize) + .divide(1000); + } + + private void updateSvpSpendTransactionValues(Keccak256 rskTxHash, BtcTransaction svpSpendTransactionUnsigned) { + provider.setSvpSpendTxHashUnsigned(svpSpendTransactionUnsigned.getHash()); + provider.setSvpSpendTxWaitingForSignatures( + new AbstractMap.SimpleEntry<>(rskTxHash, svpSpendTransactionUnsigned) + ); + + provider.setSvpFundTxSigned(null); } protected void updateFederationCreationBlockHeights() { @@ -1078,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); @@ -1090,38 +1320,11 @@ private void migrateFunds( selectedUTXOs.size() ); - // Add the TX to the release set - if (activations.isActive(ConsensusRule.RSKIP146)) { - Coin amountMigrated = selectedUTXOs.stream() - .map(UTXO::getValue) - .reduce(Coin.ZERO, Coin::add); - pegoutsWaitingForConfirmations.add(migrationTransaction, rskExecutionBlock.getNumber(), rskTxHash); - // Log the Release request - logger.debug( - "[migrateFunds] release requested. rskTXHash: {}, btcTxHash: {}, amount: {}", - rskTxHash, - migrationTransaction.getHash(), - amountMigrated - ); - eventLogger.logReleaseBtcRequested(rskTxHash.getBytes(), migrationTransaction, amountMigrated); - } else { - pegoutsWaitingForConfirmations.add(migrationTransaction, rskExecutionBlock.getNumber()); - } - - // Store pegoutTxSigHash to be able to identify the tx type - savePegoutTxSigHash(migrationTransaction); - - // Mark UTXOs as spent - availableUTXOs.removeIf(utxo -> selectedUTXOs.stream().anyMatch(selectedUtxo -> - utxo.getHash().equals(selectedUtxo.getHash()) && utxo.getIndex() == selectedUtxo.getIndex() - )); - - if (!activations.isActive(RSKIP428)) { - return; - } + Coin amountMigrated = selectedUTXOs.stream() + .map(UTXO::getValue) + .reduce(Coin.ZERO, Coin::add); - List outpointValues = extractOutpointValues(migrationTransaction); - eventLogger.logPegoutTransactionCreated(migrationTransaction.getHash(), outpointValues); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, migrationTransaction, rskTxHash, amountMigrated); } /** @@ -1167,30 +1370,81 @@ private void processPegoutRequests(Transaction rskTx) { } } - private void addToPegoutsWaitingForConfirmations( - BtcTransaction generatedTransaction, - PegoutsWaitingForConfirmations pegoutWaitingForConfirmations, - Keccak256 pegoutCreationRskTxHash, - Coin amount - ) { - if (activations.isActive(ConsensusRule.RSKIP146)) { - // Add the TX - pegoutWaitingForConfirmations.add(generatedTransaction, rskExecutionBlock.getNumber(), pegoutCreationRskTxHash); - // For a short time period, there could be items in the pegout request queue that don't have the pegoutCreationRskTxHash - // (these are pegouts created right before the consensus rule activation, that weren't processed before its activation) - // We shouldn't generate the event for those pegouts - if (pegoutCreationRskTxHash != null) { - eventLogger.logReleaseBtcRequested(pegoutCreationRskTxHash.getBytes(), generatedTransaction, amount); - } - } else { - pegoutWaitingForConfirmations.add(generatedTransaction, rskExecutionBlock.getNumber()); + 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) { + long rskExecutionBlockNumber = rskExecutionBlock.getNumber(); + + if (!activations.isActive(RSKIP146)) { + pegoutsWaitingForConfirmations.add(pegoutTransaction, rskExecutionBlockNumber); + return; } + pegoutsWaitingForConfirmations.add(pegoutTransaction, rskExecutionBlockNumber, releaseCreationTxHash); + } + + private void savePegoutTxSigHash(BtcTransaction pegoutTx) { + if (!activations.isActive(ConsensusRule.RSKIP379)){ + return; + } + Optional pegoutTxSigHash = BitcoinUtils.getFirstInputSigHash(pegoutTx); + if (!pegoutTxSigHash.isPresent()){ + throw new IllegalStateException(String.format("SigHash could not be obtained from btc tx %s", pegoutTx.getHash())); + } + provider.setPegoutTxSigHash(pegoutTxSigHash.get()); + } + + private void logReleaseRequested(Keccak256 releaseCreationTxHash, BtcTransaction pegoutTransaction, Coin requestedAmount) { + if (!activations.isActive(ConsensusRule.RSKIP146)) { + return; + } + // For a short time period, there could be items in the pegout request queue + // that were created but not processed before the consensus rule activation + // These pegouts won't have the pegoutCreationRskTxHash, + // so we shouldn't generate the event for them + if (isNull(releaseCreationTxHash)) { + return; + } + + logger.debug( + "[logReleaseRequested] release requested. rskTXHash: {}, btcTxHash: {}, amount: {}", + releaseCreationTxHash, pegoutTransaction.getHash(), requestedAmount + ); + + byte[] rskTxHashSerialized = releaseCreationTxHash.getBytes(); + eventLogger.logReleaseBtcRequested(rskTxHashSerialized, pegoutTransaction, requestedAmount); + } + + private void logPegoutTransactionCreated(BtcTransaction pegoutTransaction) { + if (!activations.isActive(RSKIP428)) { + return; + } + + List outpointValues = extractOutpointValues(pegoutTransaction); + Sha256Hash pegoutTransactionHash = pegoutTransaction.getHash(); + eventLogger.logPegoutTransactionCreated(pegoutTransactionHash, outpointValues); } private void processPegoutsIndividually( ReleaseRequestQueue pegoutRequests, ReleaseTransactionBuilder txBuilder, - List availableUTXOs, + List utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, Wallet wallet ) { @@ -1214,11 +1468,8 @@ private void processPegoutsIndividually( } BtcTransaction generatedTransaction = result.getBtcTx(); - addToPegoutsWaitingForConfirmations(generatedTransaction, pegoutsWaitingForConfirmations, pegoutRequest.getRskTxHash(), pegoutRequest.getAmount()); - - // Mark UTXOs as spent - List selectedUTXOs = result.getSelectedUTXOs(); - availableUTXOs.removeAll(selectedUTXOs); + Keccak256 pegoutCreationTxHash = pegoutRequest.getRskTxHash(); + settleReleaseRequest(utxosToUse, pegoutsWaitingForConfirmations, generatedTransaction, pegoutCreationTxHash, pegoutRequest.getAmount()); adjustBalancesIfChangeOutputWasDust(generatedTransaction, pegoutRequest.getAmount(), wallet); @@ -1229,7 +1480,7 @@ private void processPegoutsIndividually( private void processPegoutsInBatch( ReleaseRequestQueue pegoutRequests, ReleaseTransactionBuilder txBuilder, - List availableUTXOs, + List utxosToUse, PegoutsWaitingForConfirmations pegoutsWaitingForConfirmations, Wallet wallet, Transaction rskTx) { @@ -1276,25 +1527,15 @@ private void processPegoutsInBatch( result.getBtcTx().getHash(), result.getResponseCode()); BtcTransaction batchPegoutTransaction = result.getBtcTx(); - addToPegoutsWaitingForConfirmations(batchPegoutTransaction, - pegoutsWaitingForConfirmations, rskTx.getHash(), totalPegoutValue); - savePegoutTxSigHash(batchPegoutTransaction); + Keccak256 batchPegoutCreationTxHash = rskTx.getHash(); + + 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())); - - if (activations.isActive(RSKIP428)) { - List outpointValues = extractOutpointValues(batchPegoutTransaction); - eventLogger.logPegoutTransactionCreated(batchPegoutTransaction.getHash(), outpointValues); - } + pegoutEntries.stream().map(ReleaseRequestQueue.Entry::getRskTxHash).toList()); adjustBalancesIfChangeOutputWasDust(batchPegoutTransaction, totalPegoutValue, wallet); } @@ -1307,17 +1548,6 @@ private void processPegoutsInBatch( } } - private void savePegoutTxSigHash(BtcTransaction pegoutTx) { - if (!activations.isActive(ConsensusRule.RSKIP379)){ - return; - } - Optional pegoutTxSigHash = BitcoinUtils.getFirstInputSigHash(pegoutTx); - if (!pegoutTxSigHash.isPresent()){ - throw new IllegalStateException(String.format("SigHash could not be obtained from btc tx %s", pegoutTx.getHash())); - } - provider.setPegoutTxSigHash(pegoutTxSigHash.get()); - } - /** * Processes pegout waiting for confirmations. * It basically looks for pegout transactions with enough confirmations @@ -1432,17 +1662,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 federatorPublicKey Federator who is signing - * @param signatures 1 signature per btc tx input - * @param rskTxHash The id 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 federatorPublicKey, List signatures, byte[] rskTxHash) throws Exception { + public void addSignature(BtcECKey federatorBtcPublicKey, List signatures, Keccak256 releaseCreationRskTxHash) throws IOException { + if (signatures == null || signatures.isEmpty()) { + return; + } + Context.propagate(btcContext); + if (isSvpOngoing() && isSvpSpendTx(releaseCreationRskTxHash)) { + logger.info("[addSignature] Going to sign svp spend transaction with federator public key {}", federatorBtcPublicKey); + addSvpSpendTxSignatures(federatorBtcPublicKey, signatures); + return; + } + + logger.info("[addSignature] Going to sign release transaction with federator public key {}", federatorBtcPublicKey); + addReleaseSignatures(federatorBtcPublicKey, signatures, releaseCreationRskTxHash); + } + + private void addReleaseSignatures( + BtcECKey federatorPublicKey, + List signatures, + 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.isPresent()) { + 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; @@ -1450,37 +1709,29 @@ public void addSignature(BtcECKey federatorPublicKey, List signatures, b Federation federation = optionalFederation.get(); Optional federationMember = federation.getMemberByBtcPublicKey(federatorPublicKey); - if (!federationMember.isPresent()){ + 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(); - BtcTransaction btcTx = provider.getPegoutsWaitingForSignatures().get(new Keccak256(rskTxHash)); - if (btcTx == null) { - logger.warn( - "No tx waiting for signature for hash {}. Probably fully signed already.", - new Keccak256(rskTxHash) - ); - return; - } - if (btcTx.getInputs().size() != signatures.size()) { - logger.warn( - "Expected {} signatures but received {}.", - btcTx.getInputs().size(), - signatures.size() - ); - return; + byte[] releaseCreationRskTxHashSerialized = releaseCreationRskTxHash.getBytes(); + if (!activations.isActive(ConsensusRule.RSKIP326)) { + eventLogger.logAddSignature(signingFederationMember, releaseTx, releaseCreationRskTxHashSerialized); } - if (!activations.isActive(ConsensusRule.RSKIP326)) { - eventLogger.logAddSignature(signingFederationMember, btcTx, rskTxHash); + processSigning(signingFederationMember, signatures, releaseCreationRskTxHash, releaseTx); + + if (!BridgeUtils.hasEnoughSignatures(btcContext, releaseTx)) { + logMissingSignatures(releaseTx, releaseCreationRskTxHash, federation); + return; } - processSigning(signingFederationMember, signatures, rskTxHash, btcTx, federation); + logReleaseBtc(releaseTx, releaseCreationRskTxHashSerialized); + provider.getPegoutsWaitingForSignatures().remove(releaseCreationRskTxHash); } private Optional getFederationFromPublicKey(BtcECKey federatorPublicKey) { @@ -1497,119 +1748,194 @@ private Optional getFederationFromPublicKey(BtcECKey federatorPublic return Optional.empty(); } + 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.")); + 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.")); + + Keccak256 svpSpendTxCreationRskTxHash = svpSpendTxWFS.getKey(); + BtcTransaction svpSpendTx = svpSpendTxWFS.getValue(); + + if (!areSignaturesEnoughToSignAllTxInputs(svpSpendTx, signatures)) { + return; + } + + 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; + } + + logReleaseBtc(svpSpendTx, svpSpendTxCreationRskTxHash.getBytes()); + provider.clearSvpSpendTxWaitingForSignatures(); + } + + 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("[logMissingSignatures] Tx {} not yet fully signed. Requires {}/{} signatures but has {}", + releaseCreationRskTxHash, neededSignatures, federation.getSize(), signaturesCount); + } + + 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, - byte[] rskTxHash, - BtcTransaction btcTx, - Federation federation) throws IOException { + Keccak256 releaseCreationRskTxHash, + BtcTransaction btcTx) { - BtcECKey federatorBtcPublicKey = federatorMember.getBtcPublicKey(); // Build input hashes for signatures - int numInputs = btcTx.getInputs().size(); - - List sighashes = new ArrayList<>(); - List txSigs = new ArrayList<>(); - for (int i = 0; i < numInputs; i++) { - TransactionInput txIn = btcTx.getInput(i); - Script inputScript = txIn.getScriptSig(); - List chunks = inputScript.getChunks(); - byte[] program = chunks.get(chunks.size() - 1).data; - Script redeemScript = new Script(program); - sighashes.add(btcTx.hashForSignature(i, redeemScript, BtcTransaction.SigHash.ALL, false)); + List sigHashes = new ArrayList<>(); + for (int i = 0; i < btcTx.getInputs().size(); i++) { + Sha256Hash sigHash = generateSigHashForP2SHTransactionInput(btcTx, i); + sigHashes.add(sigHash); } // Verify given signatures are correct before proceeding - for (int i = 0; i < numInputs; i++) { - BtcECKey.ECDSASignature sig; - try { - sig = BtcECKey.ECDSASignature.decodeFromDER(signatures.get(i)); - } catch (RuntimeException e) { - logger.warn( - "Malformed signature for input {} of tx {}: {}", - i, - new Keccak256(rskTxHash), - Bytes.of(signatures.get(i)) - ); - return; - } + BtcECKey federatorBtcPublicKey = federatorMember.getBtcPublicKey(); + List txSigs; + try { + txSigs = getTransactionSignatures(federatorBtcPublicKey, sigHashes, signatures); + } catch (SignatureException e) { + logger.error("[processSigning] Unable to proceed with signing as the transaction signatures are incorrect. {} ", e.getMessage()); + return; + } - Sha256Hash sighash = sighashes.get(i); + // All signatures are correct. Proceed to signing + boolean signed = sign(federatorBtcPublicKey, txSigs, sigHashes, releaseCreationRskTxHash, btcTx); - if (!federatorBtcPublicKey.verify(sighash, sig)) { + if (signed && activations.isActive(ConsensusRule.RSKIP326)) { + eventLogger.logAddSignature(federatorMember, btcTx, releaseCreationRskTxHash.getBytes()); + } + } + + private List getTransactionSignatures(BtcECKey federatorBtcPublicKey, List sigHashes, List signatures) throws SignatureException { + List decodedSignatures = getDecodedSignatures(signatures); + List txSigs = new ArrayList<>(); + + for (int i = 0; i < decodedSignatures.size(); i++) { + BtcECKey.ECDSASignature decodedSignature = decodedSignatures.get(i); + Sha256Hash sigHash = sigHashes.get(i); + + 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(sig.encodeToDER()), - sighash, + Bytes.of(decodedSignature.encodeToDER()), + sigHash, federatorBtcPublicKey ); - return; + throw new SignatureException(); } - TransactionSignature txSig = new TransactionSignature(sig, BtcTransaction.SigHash.ALL, false); - txSigs.add(txSig); + TransactionSignature txSig = new TransactionSignature(decodedSignature, BtcTransaction.SigHash.ALL, false); if (!txSig.isCanonical()) { - logger.warn("Signature {} {} is not canonical.", i, Bytes.of(signatures.get(i))); - return; + logger.warn("[getTransactionSignatures] Signature {} {} is not canonical.", i, Bytes.of(decodedSignature.encodeToDER())); + throw new SignatureException(); } + txSigs.add(txSig); } + return txSigs; + } - boolean signed = false; + private List getDecodedSignatures(List signatures) throws SignatureException { + List decodedSignatures = new ArrayList<>(); + for (byte[] signature : signatures) { + try { + decodedSignatures.add(BtcECKey.ECDSASignature.decodeFromDER(signature)); + } catch (RuntimeException e) { + int index = signatures.indexOf(signature); + logger.warn("[getDecodedSignatures] Malformed signature for input {} : {}", index, Bytes.of(signature)); + throw new SignatureException(); + } + } + return decodedSignatures; + } - // All signatures are correct. Proceed to signing - for (int i = 0; i < numInputs; i++) { - Sha256Hash sighash = sighashes.get(i); + private boolean sign( + BtcECKey federatorBtcPublicKey, + List txSigs, + List sigHashes, + Keccak256 releaseCreationRskTxHash, + BtcTransaction btcTx) { + + boolean signed = false; + for (int i = 0; i < sigHashes.size(); i++) { + Sha256Hash sigHash = sigHashes.get(i); TransactionInput input = btcTx.getInput(i); Script inputScript = input.getScriptSig(); - boolean alreadySignedByThisFederator = BridgeUtils.isInputSignedByThisFederator( - federatorBtcPublicKey, - sighash, - input); + boolean alreadySignedByThisFederator = + BridgeUtils.isInputSignedByThisFederator(federatorBtcPublicKey, sigHash, input); - // Sign the input if it wasn't already - if (!alreadySignedByThisFederator) { - try { - int sigIndex = inputScript.getSigInsertionIndex(sighash, federatorBtcPublicKey); - inputScript = ScriptBuilder.updateScriptWithSignature(inputScript, txSigs.get(i).encodeToBitcoin(), sigIndex, 1, 1); - input.setScriptSig(inputScript); - logger.debug("Tx input {} for tx {} signed.", i, new Keccak256(rskTxHash)); - 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"); - return; - } 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"); - return; - } - throw e; - } - } else { - logger.warn("Input {} of tx {} already signed by this federator.", i, new Keccak256(rskTxHash)); + if (alreadySignedByThisFederator) { + logger.warn("[sign] Input {} of tx {} already signed by this federator.", i, releaseCreationRskTxHash); break; } - } - - if(signed && activations.isActive(ConsensusRule.RSKIP326)) { - eventLogger.logAddSignature(federatorMember, btcTx, rskTxHash); - } - - if (BridgeUtils.hasEnoughSignatures(btcContext, btcTx)) { - logger.info("Tx fully signed {}. Hex: {}", btcTx, Bytes.of(btcTx.bitcoinSerialize())); - provider.getPegoutsWaitingForSignatures().remove(new Keccak256(rskTxHash)); - eventLogger.logReleaseBtc(btcTx, rskTxHash); - } else if (logger.isDebugEnabled()) { - int missingSignatures = BridgeUtils.countMissingSignatures(btcContext, btcTx); - int neededSignatures = federation.getNumberOfSignaturesRequired(); - int signaturesCount = neededSignatures - missingSignatures; + Optional