@keep-network/tbtc-v2 0.1.1-dev.31 → 0.1.1-dev.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/artifacts/TBTC.json +3 -3
- package/artifacts/TBTCToken.json +3 -3
- package/artifacts/VendingMachine.json +10 -10
- package/artifacts/solcInputs/{d9d0873fb324e55f80390065c7fe0dff.json → 590fbe282bf8b630b15f2da419b402f1.json} +3 -3
- package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
- package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
- package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.json +176 -39
- package/build/contracts/bridge/Bridge.sol/IRelay.dbg.json +1 -1
- package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +1 -1
- package/build/contracts/bridge/Frauds.sol/Frauds.dbg.json +1 -1
- package/build/contracts/bridge/Frauds.sol/Frauds.json +2 -2
- package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
- package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +1 -1
- package/build/contracts/bridge/Wallets.sol/Wallets.json +2 -2
- package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
- package/build/contracts/vault/IVault.sol/IVault.dbg.json +1 -1
- package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
- package/contracts/bridge/Bridge.sol +319 -37
- package/contracts/bridge/Wallets.sol +89 -19
- package/package.json +1 -1
|
@@ -58,6 +58,11 @@ interface IRelay {
|
|
|
58
58
|
/// wallet informs the Bridge about the sweep increasing appropriate
|
|
59
59
|
/// balances in the Bank.
|
|
60
60
|
/// @dev Bridge is an upgradeable component of the Bank.
|
|
61
|
+
///
|
|
62
|
+
/// TODO: All wallets-related operations that are currently done directly
|
|
63
|
+
/// by the Bridge can be probably delegated to the Wallets library.
|
|
64
|
+
/// Examples of such operations are main UTXO or pending redemptions
|
|
65
|
+
/// value updates.
|
|
61
66
|
contract Bridge is Ownable, EcdsaWalletOwner {
|
|
62
67
|
using BTCUtils for bytes;
|
|
63
68
|
using BTCUtils for uint256;
|
|
@@ -211,6 +216,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
211
216
|
/// each swept deposit being part of the given sweep
|
|
212
217
|
/// transaction. If the maximum BTC transaction fee is exceeded,
|
|
213
218
|
/// such transaction is considered a fraud.
|
|
219
|
+
/// @dev This is a per-deposit input max fee for the sweep transaction.
|
|
214
220
|
uint64 public depositTxMaxFee;
|
|
215
221
|
|
|
216
222
|
/// TODO: Make it governable.
|
|
@@ -236,6 +242,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
236
242
|
/// each redemption request being part of the given redemption
|
|
237
243
|
/// transaction. If the maximum BTC transaction fee is exceeded, such
|
|
238
244
|
/// transaction is considered a fraud.
|
|
245
|
+
/// @dev This is a per-redemption output max fee for the redemption transaction.
|
|
239
246
|
uint64 public redemptionTxMaxFee;
|
|
240
247
|
|
|
241
248
|
/// TODO: Make it governable.
|
|
@@ -246,6 +253,15 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
246
253
|
/// to the redeemer in full amount.
|
|
247
254
|
uint256 public redemptionTimeout;
|
|
248
255
|
|
|
256
|
+
/// TODO: Make it governable.
|
|
257
|
+
/// @notice Maximum amount of the total BTC transaction fee that is
|
|
258
|
+
/// acceptable in a single moving funds transaction.
|
|
259
|
+
/// @dev This is a TOTAL max fee for the moving funds transaction. Note that
|
|
260
|
+
/// `depositTxMaxFee` is per single deposit and `redemptionTxMaxFee`
|
|
261
|
+
/// if per single redemption. `movingFundsTxMaxTotalFee` is a total fee
|
|
262
|
+
/// for the entire transaction.
|
|
263
|
+
uint64 public movingFundsTxMaxTotalFee;
|
|
264
|
+
|
|
249
265
|
/// @notice Indicates if the vault with the given address is trusted or not.
|
|
250
266
|
/// Depositors can route their revealed deposits only to trusted
|
|
251
267
|
/// vaults and have trusted vaults notified about new deposits as
|
|
@@ -263,8 +279,6 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
263
279
|
/// validating them before attempting to execute a sweep.
|
|
264
280
|
mapping(uint256 => DepositRequest) public deposits;
|
|
265
281
|
|
|
266
|
-
//TODO: Remember to update this map when implementing transferring funds
|
|
267
|
-
// between wallets (insert the main UTXO that was used as the input).
|
|
268
282
|
/// @notice Collection of main UTXOs that are honestly spent indexed by
|
|
269
283
|
/// keccak256(fundingTxHash | fundingOutputIndex). The fundingTxHash
|
|
270
284
|
/// is bytes32 (ordered as in Bitcoin internally) and
|
|
@@ -406,6 +420,11 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
406
420
|
bytes32 sighash
|
|
407
421
|
);
|
|
408
422
|
|
|
423
|
+
event MovingFundsCompleted(
|
|
424
|
+
bytes20 walletPubKeyHash,
|
|
425
|
+
bytes32 movingFundsTxHash
|
|
426
|
+
);
|
|
427
|
+
|
|
409
428
|
constructor(
|
|
410
429
|
address _bank,
|
|
411
430
|
address _relay,
|
|
@@ -426,12 +445,15 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
426
445
|
|
|
427
446
|
// TODO: Revisit initial values.
|
|
428
447
|
depositDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
|
|
429
|
-
depositTxMaxFee =
|
|
448
|
+
depositTxMaxFee = 10000; // 10000 satoshi
|
|
430
449
|
depositTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
|
|
431
450
|
redemptionDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
|
|
432
451
|
redemptionTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
|
|
433
|
-
redemptionTxMaxFee =
|
|
452
|
+
redemptionTxMaxFee = 10000; // 10000 satoshi
|
|
434
453
|
redemptionTimeout = 172800; // 48 hours
|
|
454
|
+
movingFundsTxMaxTotalFee = 10000; // 10000 satoshi
|
|
455
|
+
|
|
456
|
+
// TODO: Revisit initial values.
|
|
435
457
|
frauds.setSlashingAmount(10000 * 1e18); // 10000 T
|
|
436
458
|
frauds.setNotifierRewardMultiplier(100); // 100%
|
|
437
459
|
frauds.setChallengeDefeatTimeout(7 days);
|
|
@@ -1595,9 +1617,9 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1595
1617
|
proofDifficultyContext()
|
|
1596
1618
|
);
|
|
1597
1619
|
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1620
|
+
// Process the redemption transaction input. Specifically, check if it
|
|
1621
|
+
// refers to the expected wallet's main UTXO.
|
|
1622
|
+
processWalletOutboundTxInput(
|
|
1601
1623
|
redemptionTx.inputVector,
|
|
1602
1624
|
mainUtxo,
|
|
1603
1625
|
walletPubKeyHash
|
|
@@ -1646,10 +1668,14 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1646
1668
|
bank.transferBalance(treasury, outputsInfo.totalTreasuryFee);
|
|
1647
1669
|
}
|
|
1648
1670
|
|
|
1649
|
-
/// @notice
|
|
1650
|
-
///
|
|
1651
|
-
///
|
|
1652
|
-
///
|
|
1671
|
+
/// @notice Checks whether an outbound Bitcoin transaction performed from
|
|
1672
|
+
/// the given wallet has an input vector that contains a single
|
|
1673
|
+
/// input referring to the wallet's main UTXO. Marks that main UTXO
|
|
1674
|
+
/// as correctly spent if the validation succeeds. Reverts otherwise.
|
|
1675
|
+
/// There are two outbound transactions from a wallet possible: a
|
|
1676
|
+
/// redemption transaction or a moving funds to another wallet
|
|
1677
|
+
/// transaction.
|
|
1678
|
+
/// @param walletOutboundTxInputVector Bitcoin outbound transaction's input
|
|
1653
1679
|
/// vector. This function assumes vector's structure is valid so it
|
|
1654
1680
|
/// must be validated using e.g. `BTCUtils.validateVin` function
|
|
1655
1681
|
/// before it is passed here
|
|
@@ -1657,9 +1683,9 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1657
1683
|
/// the Ethereum chain.
|
|
1658
1684
|
/// @param walletPubKeyHash 20-byte public key hash (computed using Bitcoin
|
|
1659
1685
|
// HASH160 over the compressed ECDSA public key) of the wallet which
|
|
1660
|
-
/// performed the
|
|
1661
|
-
function
|
|
1662
|
-
bytes memory
|
|
1686
|
+
/// performed the outbound transaction.
|
|
1687
|
+
function processWalletOutboundTxInput(
|
|
1688
|
+
bytes memory walletOutboundTxInputVector,
|
|
1663
1689
|
BitcoinTx.UTXO calldata mainUtxo,
|
|
1664
1690
|
bytes20 walletPubKeyHash
|
|
1665
1691
|
) internal {
|
|
@@ -1682,16 +1708,16 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1682
1708
|
"Invalid main UTXO data"
|
|
1683
1709
|
);
|
|
1684
1710
|
|
|
1685
|
-
// Assert that the single
|
|
1711
|
+
// Assert that the single outbound transaction input actually
|
|
1686
1712
|
// refers to the wallet's main UTXO.
|
|
1687
1713
|
(
|
|
1688
|
-
bytes32
|
|
1689
|
-
uint32
|
|
1690
|
-
) =
|
|
1714
|
+
bytes32 outpointTxHash,
|
|
1715
|
+
uint32 outpointIndex
|
|
1716
|
+
) = parseWalletOutboundTxInput(walletOutboundTxInputVector);
|
|
1691
1717
|
require(
|
|
1692
|
-
mainUtxo.txHash ==
|
|
1693
|
-
mainUtxo.txOutputIndex ==
|
|
1694
|
-
"
|
|
1718
|
+
mainUtxo.txHash == outpointTxHash &&
|
|
1719
|
+
mainUtxo.txOutputIndex == outpointIndex,
|
|
1720
|
+
"Outbound transaction input must point to the wallet's main UTXO"
|
|
1695
1721
|
);
|
|
1696
1722
|
|
|
1697
1723
|
// Main UTXO used as an input, mark it as spent.
|
|
@@ -1704,10 +1730,13 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1704
1730
|
] = true;
|
|
1705
1731
|
}
|
|
1706
1732
|
|
|
1707
|
-
/// @notice
|
|
1708
|
-
///
|
|
1709
|
-
/// index from its outpoint.
|
|
1710
|
-
///
|
|
1733
|
+
/// @notice Parses the input vector of an outbound Bitcoin transaction
|
|
1734
|
+
/// performed from the given wallet. It extracts the single input
|
|
1735
|
+
/// then the transaction hash and output index from its outpoint.
|
|
1736
|
+
/// There are two outbound transactions from a wallet possible: a
|
|
1737
|
+
/// redemption transaction or a moving funds to another wallet
|
|
1738
|
+
/// transaction.
|
|
1739
|
+
/// @param walletOutboundTxInputVector Bitcoin outbound transaction input
|
|
1711
1740
|
/// vector. This function assumes vector's structure is valid so it
|
|
1712
1741
|
/// must be validated using e.g. `BTCUtils.validateVin` function
|
|
1713
1742
|
/// before it is passed here
|
|
@@ -1715,12 +1744,10 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1715
1744
|
/// pointed in the input's outpoint.
|
|
1716
1745
|
/// @return outpointIndex 4-byte index of the Bitcoin transaction output
|
|
1717
1746
|
/// which is pointed in the input's outpoint.
|
|
1718
|
-
function
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
{
|
|
1723
|
-
// To determine the total number of redemption transaction inputs,
|
|
1747
|
+
function parseWalletOutboundTxInput(
|
|
1748
|
+
bytes memory walletOutboundTxInputVector
|
|
1749
|
+
) internal pure returns (bytes32 outpointTxHash, uint32 outpointIndex) {
|
|
1750
|
+
// To determine the total number of Bitcoin transaction inputs,
|
|
1724
1751
|
// we need to parse the compactSize uint (VarInt) the input vector is
|
|
1725
1752
|
// prepended by. That compactSize uint encodes the number of vector
|
|
1726
1753
|
// elements using the format presented in:
|
|
@@ -1728,13 +1755,13 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1728
1755
|
// We don't need asserting the compactSize uint is parseable since it
|
|
1729
1756
|
// was already checked during `validateVin` validation.
|
|
1730
1757
|
// See `BitcoinTx.inputVector` docs for more details.
|
|
1731
|
-
(, uint256 inputsCount) =
|
|
1758
|
+
(, uint256 inputsCount) = walletOutboundTxInputVector.parseVarInt();
|
|
1732
1759
|
require(
|
|
1733
1760
|
inputsCount == 1,
|
|
1734
|
-
"
|
|
1761
|
+
"Outbound transaction must have a single input"
|
|
1735
1762
|
);
|
|
1736
1763
|
|
|
1737
|
-
bytes memory input =
|
|
1764
|
+
bytes memory input = walletOutboundTxInputVector.extractInputAtIndex(0);
|
|
1738
1765
|
|
|
1739
1766
|
outpointTxHash = input.extractInputTxIdLE();
|
|
1740
1767
|
|
|
@@ -1797,11 +1824,26 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1797
1824
|
// scripts that can be used to lock the change. This is done upfront to
|
|
1798
1825
|
// save on gas. Both scripts have a strict format defined by Bitcoin.
|
|
1799
1826
|
//
|
|
1800
|
-
// The P2PKH script has format <0x1976a914> <20-byte PKH> <0x88ac>.
|
|
1827
|
+
// The P2PKH script has the byte format: <0x1976a914> <20-byte PKH> <0x88ac>.
|
|
1828
|
+
// According to https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
|
|
1829
|
+
// - 0x19: Byte length of the entire script
|
|
1830
|
+
// - 0x76: OP_DUP
|
|
1831
|
+
// - 0xa9: OP_HASH160
|
|
1832
|
+
// - 0x14: Byte length of the public key hash
|
|
1833
|
+
// - 0x88: OP_EQUALVERIFY
|
|
1834
|
+
// - 0xac: OP_CHECKSIG
|
|
1835
|
+
// which matches the P2PKH structure as per:
|
|
1836
|
+
// https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash
|
|
1801
1837
|
bytes32 walletP2PKHScriptKeccak = keccak256(
|
|
1802
1838
|
abi.encodePacked(hex"1976a914", walletPubKeyHash, hex"88ac")
|
|
1803
1839
|
);
|
|
1804
|
-
// The P2WPKH script has format <0x160014> <20-byte PKH>.
|
|
1840
|
+
// The P2WPKH script has the byte format: <0x160014> <20-byte PKH>.
|
|
1841
|
+
// According to https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
|
|
1842
|
+
// - 0x16: Byte length of the entire script
|
|
1843
|
+
// - 0x00: OP_0
|
|
1844
|
+
// - 0x14: Byte length of the public key hash
|
|
1845
|
+
// which matches the P2WPKH structure as per:
|
|
1846
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#P2WPKH
|
|
1805
1847
|
bytes32 walletP2WPKHScriptKeccak = keccak256(
|
|
1806
1848
|
abi.encodePacked(hex"160014", walletPubKeyHash)
|
|
1807
1849
|
);
|
|
@@ -1827,7 +1869,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
1827
1869
|
// Extract the value from given output.
|
|
1828
1870
|
uint64 outputValue = output.extractValue();
|
|
1829
1871
|
// The output consists of an 8-byte value and a variable length
|
|
1830
|
-
// script. To extract that script we slice the output
|
|
1872
|
+
// script. To extract that script we slice the output starting from
|
|
1831
1873
|
// 9th byte until the end.
|
|
1832
1874
|
bytes memory outputScript = output.slice(8, output.length - 8);
|
|
1833
1875
|
|
|
@@ -2006,4 +2048,244 @@ contract Bridge is Ownable, EcdsaWalletOwner {
|
|
|
2006
2048
|
// Return the requested amount of tokens to the redeemer
|
|
2007
2049
|
bank.transferBalance(request.redeemer, request.requestedAmount);
|
|
2008
2050
|
}
|
|
2051
|
+
|
|
2052
|
+
/// @notice Used by the wallet to prove the BTC moving funds transaction
|
|
2053
|
+
/// and to make the necessary state changes. Moving funds is only
|
|
2054
|
+
/// accepted if it satisfies SPV proof.
|
|
2055
|
+
///
|
|
2056
|
+
/// The function validates the moving funds transaction structure
|
|
2057
|
+
/// by checking if it actually spends the main UTXO of the declared
|
|
2058
|
+
/// wallet and locks the value on the pre-committed target wallets
|
|
2059
|
+
/// using a reasonable transaction fee. If all preconditions are
|
|
2060
|
+
/// met, this functions closes the source wallet.
|
|
2061
|
+
///
|
|
2062
|
+
/// It is possible to prove the given moving funds transaction only
|
|
2063
|
+
/// one time.
|
|
2064
|
+
/// @param movingFundsTx Bitcoin moving funds transaction data
|
|
2065
|
+
/// @param movingFundsProof Bitcoin moving funds proof data
|
|
2066
|
+
/// @param mainUtxo Data of the wallet's main UTXO, as currently known on
|
|
2067
|
+
/// the Ethereum chain
|
|
2068
|
+
/// @param walletPubKeyHash 20-byte public key hash (computed using Bitcoin
|
|
2069
|
+
/// HASH160 over the compressed ECDSA public key) of the wallet
|
|
2070
|
+
/// which performed the moving funds transaction
|
|
2071
|
+
/// @dev Requirements:
|
|
2072
|
+
/// - `movingFundsTx` components must match the expected structure. See
|
|
2073
|
+
/// `BitcoinTx.Info` docs for reference. Their values must exactly
|
|
2074
|
+
/// correspond to appropriate Bitcoin transaction fields to produce
|
|
2075
|
+
/// a provable transaction hash.
|
|
2076
|
+
/// - The `movingFundsTx` should represent a Bitcoin transaction with
|
|
2077
|
+
/// exactly 1 input that refers to the wallet's main UTXO. That
|
|
2078
|
+
/// transaction should have 1..n outputs corresponding to the
|
|
2079
|
+
/// pre-committed target wallets. Outputs must be ordered in the
|
|
2080
|
+
/// same way as their corresponding target wallets are ordered
|
|
2081
|
+
/// within the target wallets commitment.
|
|
2082
|
+
/// - `movingFundsProof` components must match the expected structure.
|
|
2083
|
+
/// See `BitcoinTx.Proof` docs for reference. The `bitcoinHeaders`
|
|
2084
|
+
/// field must contain a valid number of block headers, not less
|
|
2085
|
+
/// than the `txProofDifficultyFactor` contract constant.
|
|
2086
|
+
/// - `mainUtxo` components must point to the recent main UTXO
|
|
2087
|
+
/// of the given wallet, as currently known on the Ethereum chain.
|
|
2088
|
+
/// Additionally, the recent main UTXO on Ethereum must be set.
|
|
2089
|
+
/// - `walletPubKeyHash` must be connected with the main UTXO used
|
|
2090
|
+
/// as transaction single input.
|
|
2091
|
+
/// - The wallet that `walletPubKeyHash` points to must be in the
|
|
2092
|
+
/// MovingFunds state.
|
|
2093
|
+
/// - The target wallets commitment must be submitted by the wallet
|
|
2094
|
+
/// that `walletPubKeyHash` points to.
|
|
2095
|
+
/// - The total Bitcoin transaction fee must be lesser or equal
|
|
2096
|
+
/// to `movingFundsTxMaxTotalFee` governable parameter.
|
|
2097
|
+
function submitMovingFundsProof(
|
|
2098
|
+
BitcoinTx.Info calldata movingFundsTx,
|
|
2099
|
+
BitcoinTx.Proof calldata movingFundsProof,
|
|
2100
|
+
BitcoinTx.UTXO calldata mainUtxo,
|
|
2101
|
+
bytes20 walletPubKeyHash
|
|
2102
|
+
) external {
|
|
2103
|
+
// The actual transaction proof is performed here. After that point, we
|
|
2104
|
+
// can assume the transaction happened on Bitcoin chain and has
|
|
2105
|
+
// a sufficient number of confirmations as determined by
|
|
2106
|
+
// `txProofDifficultyFactor` constant.
|
|
2107
|
+
bytes32 movingFundsTxHash = BitcoinTx.validateProof(
|
|
2108
|
+
movingFundsTx,
|
|
2109
|
+
movingFundsProof,
|
|
2110
|
+
proofDifficultyContext()
|
|
2111
|
+
);
|
|
2112
|
+
|
|
2113
|
+
// Process the moving funds transaction input. Specifically, check if
|
|
2114
|
+
// it refers to the expected wallet's main UTXO.
|
|
2115
|
+
processWalletOutboundTxInput(
|
|
2116
|
+
movingFundsTx.inputVector,
|
|
2117
|
+
mainUtxo,
|
|
2118
|
+
walletPubKeyHash
|
|
2119
|
+
);
|
|
2120
|
+
|
|
2121
|
+
(
|
|
2122
|
+
bytes32 targetWalletsHash,
|
|
2123
|
+
uint256 outputsTotalValue
|
|
2124
|
+
) = processMovingFundsTxOutputs(movingFundsTx.outputVector);
|
|
2125
|
+
|
|
2126
|
+
require(
|
|
2127
|
+
mainUtxo.txOutputValue - outputsTotalValue <=
|
|
2128
|
+
movingFundsTxMaxTotalFee,
|
|
2129
|
+
"Transaction fee is too high"
|
|
2130
|
+
);
|
|
2131
|
+
|
|
2132
|
+
wallets.notifyFundsMoved(walletPubKeyHash, targetWalletsHash);
|
|
2133
|
+
|
|
2134
|
+
emit MovingFundsCompleted(walletPubKeyHash, movingFundsTxHash);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/// @notice Processes the moving funds Bitcoin transaction output vector
|
|
2138
|
+
/// and extracts information required for further processing.
|
|
2139
|
+
/// @param movingFundsTxOutputVector Bitcoin moving funds transaction output
|
|
2140
|
+
/// vector. This function assumes vector's structure is valid so it
|
|
2141
|
+
/// must be validated using e.g. `BTCUtils.validateVout` function
|
|
2142
|
+
/// before it is passed here
|
|
2143
|
+
/// @return targetWalletsHash keccak256 hash over the list of actual
|
|
2144
|
+
/// target wallets used in the transaction.
|
|
2145
|
+
/// @return outputsTotalValue Sum of all outputs values.
|
|
2146
|
+
/// @dev Requirements:
|
|
2147
|
+
/// - The `movingFundsTxOutputVector` must be parseable, i.e. must
|
|
2148
|
+
/// be validated by the caller as stated in their parameter doc.
|
|
2149
|
+
/// - Each output must refer to a 20-byte public key hash.
|
|
2150
|
+
/// - The total outputs value must be evenly divided over all outputs.
|
|
2151
|
+
function processMovingFundsTxOutputs(bytes memory movingFundsTxOutputVector)
|
|
2152
|
+
internal
|
|
2153
|
+
view
|
|
2154
|
+
returns (bytes32 targetWalletsHash, uint256 outputsTotalValue)
|
|
2155
|
+
{
|
|
2156
|
+
// Determining the total number of Bitcoin transaction outputs in
|
|
2157
|
+
// the same way as for number of inputs. See `BitcoinTx.outputVector`
|
|
2158
|
+
// docs for more details.
|
|
2159
|
+
(
|
|
2160
|
+
uint256 outputsCompactSizeUintLength,
|
|
2161
|
+
uint256 outputsCount
|
|
2162
|
+
) = movingFundsTxOutputVector.parseVarInt();
|
|
2163
|
+
|
|
2164
|
+
// To determine the first output starting index, we must jump over
|
|
2165
|
+
// the compactSize uint which prepends the output vector. One byte
|
|
2166
|
+
// must be added because `BtcUtils.parseVarInt` does not include
|
|
2167
|
+
// compactSize uint tag in the returned length.
|
|
2168
|
+
//
|
|
2169
|
+
// For >= 0 && <= 252, `BTCUtils.determineVarIntDataLengthAt`
|
|
2170
|
+
// returns `0`, so we jump over one byte of compactSize uint.
|
|
2171
|
+
//
|
|
2172
|
+
// For >= 253 && <= 0xffff there is `0xfd` tag,
|
|
2173
|
+
// `BTCUtils.determineVarIntDataLengthAt` returns `2` (no
|
|
2174
|
+
// tag byte included) so we need to jump over 1+2 bytes of
|
|
2175
|
+
// compactSize uint.
|
|
2176
|
+
//
|
|
2177
|
+
// Please refer `BTCUtils` library and compactSize uint
|
|
2178
|
+
// docs in `BitcoinTx` library for more details.
|
|
2179
|
+
uint256 outputStartingIndex = 1 + outputsCompactSizeUintLength;
|
|
2180
|
+
|
|
2181
|
+
bytes20[] memory targetWallets = new bytes20[](outputsCount);
|
|
2182
|
+
uint64[] memory outputsValues = new uint64[](outputsCount);
|
|
2183
|
+
|
|
2184
|
+
// Outputs processing loop.
|
|
2185
|
+
for (uint256 i = 0; i < outputsCount; i++) {
|
|
2186
|
+
uint256 outputLength = movingFundsTxOutputVector
|
|
2187
|
+
.determineOutputLengthAt(outputStartingIndex);
|
|
2188
|
+
|
|
2189
|
+
bytes memory output = movingFundsTxOutputVector.slice(
|
|
2190
|
+
outputStartingIndex,
|
|
2191
|
+
outputLength
|
|
2192
|
+
);
|
|
2193
|
+
|
|
2194
|
+
// Extract the output script payload.
|
|
2195
|
+
bytes memory targetWalletPubKeyHashBytes = output.extractHash();
|
|
2196
|
+
// Output script payload must refer to a known wallet public key
|
|
2197
|
+
// hash which is always 20-byte.
|
|
2198
|
+
require(
|
|
2199
|
+
targetWalletPubKeyHashBytes.length == 20,
|
|
2200
|
+
"Target wallet public key hash must have 20 bytes"
|
|
2201
|
+
);
|
|
2202
|
+
|
|
2203
|
+
bytes20 targetWalletPubKeyHash = targetWalletPubKeyHashBytes
|
|
2204
|
+
.slice20(0);
|
|
2205
|
+
|
|
2206
|
+
// The next step is making sure that the 20-byte public key hash
|
|
2207
|
+
// is actually used in the right context of a P2PKH or P2WPKH
|
|
2208
|
+
// output. To do so, we must extract the full script from the output
|
|
2209
|
+
// and compare with the expected P2PKH and P2WPKH scripts
|
|
2210
|
+
// referring to that 20-byte public key hash. The output consists
|
|
2211
|
+
// of an 8-byte value and a variable length script. To extract the
|
|
2212
|
+
// script we slice the output starting from 9th byte until the end.
|
|
2213
|
+
bytes32 outputScriptKeccak = keccak256(
|
|
2214
|
+
output.slice(8, output.length - 8)
|
|
2215
|
+
);
|
|
2216
|
+
// Build the expected P2PKH script which has the following byte
|
|
2217
|
+
// format: <0x1976a914> <20-byte PKH> <0x88ac>. According to
|
|
2218
|
+
// https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
|
|
2219
|
+
// - 0x19: Byte length of the entire script
|
|
2220
|
+
// - 0x76: OP_DUP
|
|
2221
|
+
// - 0xa9: OP_HASH160
|
|
2222
|
+
// - 0x14: Byte length of the public key hash
|
|
2223
|
+
// - 0x88: OP_EQUALVERIFY
|
|
2224
|
+
// - 0xac: OP_CHECKSIG
|
|
2225
|
+
// which matches the P2PKH structure as per:
|
|
2226
|
+
// https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash
|
|
2227
|
+
bytes32 targetWalletP2PKHScriptKeccak = keccak256(
|
|
2228
|
+
abi.encodePacked(
|
|
2229
|
+
hex"1976a914",
|
|
2230
|
+
targetWalletPubKeyHash,
|
|
2231
|
+
hex"88ac"
|
|
2232
|
+
)
|
|
2233
|
+
);
|
|
2234
|
+
// Build the expected P2WPKH script which has the following format:
|
|
2235
|
+
// <0x160014> <20-byte PKH>. According to
|
|
2236
|
+
// https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
|
|
2237
|
+
// - 0x16: Byte length of the entire script
|
|
2238
|
+
// - 0x00: OP_0
|
|
2239
|
+
// - 0x14: Byte length of the public key hash
|
|
2240
|
+
// which matches the P2WPKH structure as per:
|
|
2241
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#P2WPKH
|
|
2242
|
+
bytes32 targetWalletP2WPKHScriptKeccak = keccak256(
|
|
2243
|
+
abi.encodePacked(hex"160014", targetWalletPubKeyHash)
|
|
2244
|
+
);
|
|
2245
|
+
// Make sure the actual output script matches either the P2PKH
|
|
2246
|
+
// or P2WPKH format.
|
|
2247
|
+
require(
|
|
2248
|
+
outputScriptKeccak == targetWalletP2PKHScriptKeccak ||
|
|
2249
|
+
outputScriptKeccak == targetWalletP2WPKHScriptKeccak,
|
|
2250
|
+
"Output must be P2PKH or P2WPKH"
|
|
2251
|
+
);
|
|
2252
|
+
|
|
2253
|
+
// Add the wallet public key hash to the list that will be used
|
|
2254
|
+
// to build the result list hash. There is no need to check if
|
|
2255
|
+
// given output is a change here because the actual target wallet
|
|
2256
|
+
// list must be exactly the same as the pre-committed target wallet
|
|
2257
|
+
// list which is guaranteed to be valid.
|
|
2258
|
+
targetWallets[i] = targetWalletPubKeyHash;
|
|
2259
|
+
|
|
2260
|
+
// Extract the value from given output.
|
|
2261
|
+
outputsValues[i] = output.extractValue();
|
|
2262
|
+
outputsTotalValue += outputsValues[i];
|
|
2263
|
+
|
|
2264
|
+
// Make the `outputStartingIndex` pointing to the next output by
|
|
2265
|
+
// increasing it by current output's length.
|
|
2266
|
+
outputStartingIndex += outputLength;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// Compute the indivisible remainder that remains after dividing the
|
|
2270
|
+
// outputs total value over all outputs evenly.
|
|
2271
|
+
uint256 outputsTotalValueRemainder = outputsTotalValue % outputsCount;
|
|
2272
|
+
// Compute the minimum allowed output value by dividing the outputs
|
|
2273
|
+
// total value (reduced by the remainder) by the number of outputs.
|
|
2274
|
+
uint256 minOutputValue = (outputsTotalValue -
|
|
2275
|
+
outputsTotalValueRemainder) / outputsCount;
|
|
2276
|
+
// Maximum possible value is the minimum value with the remainder included.
|
|
2277
|
+
uint256 maxOutputValue = minOutputValue + outputsTotalValueRemainder;
|
|
2278
|
+
|
|
2279
|
+
for (uint256 i = 0; i < outputsCount; i++) {
|
|
2280
|
+
require(
|
|
2281
|
+
minOutputValue <= outputsValues[i] &&
|
|
2282
|
+
outputsValues[i] <= maxOutputValue,
|
|
2283
|
+
"Transaction amount is not distributed evenly"
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
targetWalletsHash = keccak256(abi.encodePacked(targetWallets));
|
|
2288
|
+
|
|
2289
|
+
return (targetWalletsHash, outputsTotalValue);
|
|
2290
|
+
}
|
|
2009
2291
|
}
|
|
@@ -92,9 +92,13 @@ library Wallets {
|
|
|
92
92
|
uint32 createdAt;
|
|
93
93
|
// UNIX timestamp indicating the moment the wallet was requested to
|
|
94
94
|
// move their funds.
|
|
95
|
-
uint32
|
|
95
|
+
uint32 movingFundsRequestedAt;
|
|
96
96
|
// Current state of the wallet.
|
|
97
97
|
WalletState state;
|
|
98
|
+
// Moving funds target wallet commitment submitted by the wallet. It
|
|
99
|
+
// is built by applying the keccak256 hash over the list of 20-byte
|
|
100
|
+
// public key hashes of the target wallets.
|
|
101
|
+
bytes32 movingFundsTargetWalletsCommitmentHash;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
event WalletCreationPeriodUpdated(uint32 newCreationPeriod);
|
|
@@ -308,7 +312,7 @@ library Wallets {
|
|
|
308
312
|
|
|
309
313
|
// Compress wallet's public key and calculate Bitcoin's hash160 of it.
|
|
310
314
|
bytes20 walletPubKeyHash = bytes20(
|
|
311
|
-
EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).
|
|
315
|
+
EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()
|
|
312
316
|
);
|
|
313
317
|
|
|
314
318
|
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
@@ -346,7 +350,7 @@ library Wallets {
|
|
|
346
350
|
|
|
347
351
|
// Compress wallet's public key and calculate Bitcoin's hash160 of it.
|
|
348
352
|
bytes20 walletPubKeyHash = bytes20(
|
|
349
|
-
EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).
|
|
353
|
+
EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()
|
|
350
354
|
);
|
|
351
355
|
|
|
352
356
|
require(
|
|
@@ -448,16 +452,12 @@ library Wallets {
|
|
|
448
452
|
if (wallet.mainUtxoHash == bytes32(0)) {
|
|
449
453
|
// If the wallet has no main UTXO, that means its BTC balance
|
|
450
454
|
// is zero and it should be closed immediately.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
454
|
-
|
|
455
|
-
self.registry.closeWallet(wallet.ecdsaWalletID);
|
|
455
|
+
closeWallet(self, walletPubKeyHash);
|
|
456
456
|
} else {
|
|
457
457
|
// Otherwise, initialize the moving funds process.
|
|
458
458
|
wallet.state = WalletState.MovingFunds;
|
|
459
459
|
/* solhint-disable-next-line not-rely-on-time */
|
|
460
|
-
wallet.
|
|
460
|
+
wallet.movingFundsRequestedAt = uint32(block.timestamp);
|
|
461
461
|
|
|
462
462
|
emit WalletMovingFunds(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
463
463
|
}
|
|
@@ -470,9 +470,21 @@ library Wallets {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
/// @notice Closes the given wallet and notifies the ECDSA registry
|
|
474
|
+
/// about this fact.
|
|
475
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
476
|
+
/// @dev Requirements:
|
|
477
|
+
/// - The caller must make sure that the wallet is in the
|
|
478
|
+
/// Live or MovingFunds state.
|
|
479
|
+
function closeWallet(Data storage self, bytes20 walletPubKeyHash) internal {
|
|
480
|
+
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
481
|
+
|
|
482
|
+
wallet.state = WalletState.Closed;
|
|
483
|
+
|
|
484
|
+
emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
485
|
+
|
|
486
|
+
self.registry.closeWallet(wallet.ecdsaWalletID);
|
|
487
|
+
}
|
|
476
488
|
|
|
477
489
|
/// @notice Reports about a fraud committed by the given wallet. This
|
|
478
490
|
/// function performs slashing and wallet termination in reaction
|
|
@@ -482,9 +494,19 @@ library Wallets {
|
|
|
482
494
|
/// @dev Requirements:
|
|
483
495
|
/// - Wallet must be in Live or MovingFunds state
|
|
484
496
|
function notifyFraud(Data storage self, bytes20 walletPubKeyHash) external {
|
|
485
|
-
|
|
497
|
+
WalletState walletState = self
|
|
498
|
+
.registeredWallets[walletPubKeyHash]
|
|
499
|
+
.state;
|
|
500
|
+
|
|
501
|
+
require(
|
|
502
|
+
walletState == WalletState.Live ||
|
|
503
|
+
walletState == WalletState.MovingFunds,
|
|
504
|
+
"ECDSA wallet must be in Live or MovingFunds state"
|
|
505
|
+
);
|
|
486
506
|
|
|
487
507
|
terminateWallet(self, walletPubKeyHash);
|
|
508
|
+
|
|
509
|
+
// TODO: Perform slashing of wallet operators and add unit tests for that.
|
|
488
510
|
}
|
|
489
511
|
|
|
490
512
|
/// @notice Terminates the given wallet and notifies the ECDSA registry
|
|
@@ -494,16 +516,12 @@ library Wallets {
|
|
|
494
516
|
/// creation immediately.
|
|
495
517
|
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
496
518
|
/// @dev Requirements:
|
|
497
|
-
/// -
|
|
519
|
+
/// - The caller must make sure that the wallet is in the
|
|
520
|
+
/// Live or MovingFunds state.
|
|
498
521
|
function terminateWallet(Data storage self, bytes20 walletPubKeyHash)
|
|
499
522
|
internal
|
|
500
523
|
{
|
|
501
524
|
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
502
|
-
require(
|
|
503
|
-
wallet.state == WalletState.Live ||
|
|
504
|
-
wallet.state == WalletState.MovingFunds,
|
|
505
|
-
"ECDSA wallet must be in Live or MovingFunds state"
|
|
506
|
-
);
|
|
507
525
|
|
|
508
526
|
wallet.state = WalletState.Terminated;
|
|
509
527
|
|
|
@@ -518,4 +536,56 @@ library Wallets {
|
|
|
518
536
|
|
|
519
537
|
self.registry.closeWallet(wallet.ecdsaWalletID);
|
|
520
538
|
}
|
|
539
|
+
|
|
540
|
+
/// @notice Notifies that the wallet completed the moving funds process
|
|
541
|
+
/// successfully. Checks if the funds were moved to the expected
|
|
542
|
+
/// target wallets. Closes the source wallet if everything went
|
|
543
|
+
/// good and reverts otherwise.
|
|
544
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
545
|
+
/// @param targetWalletsHash 32-byte keccak256 hash over the list of
|
|
546
|
+
/// 20-byte public key hashes of the target wallets actually used
|
|
547
|
+
/// within the moving funds transactions.
|
|
548
|
+
/// @dev Requirements:
|
|
549
|
+
/// - The caller must make sure the moving funds transaction actually
|
|
550
|
+
/// happened on Bitcoin chain and fits the protocol requirements.
|
|
551
|
+
/// - The source wallet must be in the MovingFunds state
|
|
552
|
+
/// - The target wallets commitment must be submitted by the source
|
|
553
|
+
/// wallet.
|
|
554
|
+
/// - The actual target wallets used in the moving funds transaction
|
|
555
|
+
/// must be exactly the same as the target wallets commitment.
|
|
556
|
+
function notifyFundsMoved(
|
|
557
|
+
Data storage self,
|
|
558
|
+
bytes20 walletPubKeyHash,
|
|
559
|
+
bytes32 targetWalletsHash
|
|
560
|
+
) external {
|
|
561
|
+
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
562
|
+
// Check that the wallet is in the MovingFunds state but don't check
|
|
563
|
+
// if the moving funds timeout is exceeded. That should give a
|
|
564
|
+
// possibility to move funds in case when timeout was hit but was
|
|
565
|
+
// not reported yet.
|
|
566
|
+
require(
|
|
567
|
+
wallet.state == WalletState.MovingFunds,
|
|
568
|
+
"ECDSA wallet must be in MovingFunds state"
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
bytes32 targetWalletsCommitmentHash = wallet
|
|
572
|
+
.movingFundsTargetWalletsCommitmentHash;
|
|
573
|
+
|
|
574
|
+
require(
|
|
575
|
+
targetWalletsCommitmentHash != bytes32(0),
|
|
576
|
+
"Target wallets commitment not submitted yet"
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Make sure that the target wallets where funds were moved to are
|
|
580
|
+
// exactly the same as the ones the source wallet committed to.
|
|
581
|
+
require(
|
|
582
|
+
targetWalletsCommitmentHash == targetWalletsHash,
|
|
583
|
+
"Target wallets don't correspond to the commitment"
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// If funds were moved, the wallet has no longer a main UTXO.
|
|
587
|
+
delete wallet.mainUtxoHash;
|
|
588
|
+
|
|
589
|
+
closeWallet(self, walletPubKeyHash);
|
|
590
|
+
}
|
|
521
591
|
}
|