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