diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/README.md b/pkg/coordinator/tasks/generate_shadowfork_funding/README.md new file mode 100644 index 00000000..8224dee5 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/README.md @@ -0,0 +1,51 @@ +## `generate_child_wallet` Task + +### Description +The `generate_child_wallet` task is designed to create a new, funded child wallet. This task is especially useful in scenarios requiring the setup of additional wallets for transaction testing, smart contract interactions, or other Ethereum network activities. + +### Configuration Parameters + +- **`privateKey`**: + The private key of the parent wallet used for funding the new child wallet. + +- **`walletSeed`**: + A seed phrase used for generating the child wallet. This allows for deterministic wallet creation. + +- **`randomSeed`**: + If set to `true`, the task generates the child wallet using a random seed, resulting in a non-deterministic wallet. + +- **`prefundAmount`**: + The amount of cryptocurrency to be transferred to the child wallet during prefunding. + +- **`prefundMinBalance`**: + The minimum balance threshold in the parent wallet required to execute the prefunding. Prefunding occurs only if the parent wallet's balance is above this amount. + +- **`prefundFeeCap`**: + The maximum fee cap for the prefunding transaction to the child wallet. + +- **`prefundTipCap`**: + The tip cap for the prefunding transaction, determining the priority fee. + +- **`walletAddressResultVar`**: + The name of the variable to store the address of the newly created child wallet. This can be used for reference in subsequent tasks. + +- **`walletPrivateKeyResultVar`**: + The name of the variable to store the private key of the new child wallet. This ensures the child wallet can be accessed and used in later tasks. + +### Defaults + +Default settings for the `generate_child_wallet` task: + +```yaml +- name: generate_child_wallet + config: + privateKey: "" + walletSeed: "" + randomSeed: false + prefundAmount: "1000000000000000000" + prefundMinBalance: "500000000000000000" + prefundFeeCap: "500000000000" + prefundTipCap: "1000000000" + walletAddressResultVar: "" + walletPrivateKeyResultVar: "" +``` diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/config.go b/pkg/coordinator/tasks/generate_shadowfork_funding/config.go new file mode 100644 index 00000000..c279f184 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/config.go @@ -0,0 +1,39 @@ +package generateshadowforkfunding + +import ( + "errors" + "math/big" +) + +type Config struct { + ShadowForkVaultContract string `yaml:"shadowForkVaultContract" json:"shadowForkVaultContract"` + + PrivateKey string `yaml:"privateKey" json:"privateKey"` + MinBalance *big.Int `yaml:"minBalance" json:"minBalance"` + + TxFeeCap *big.Int `yaml:"prefundFeeCap" json:"prefundFeeCap"` + TxTipCap *big.Int `yaml:"prefundTipCap" json:"prefundTipCap"` + RequestAmount *big.Int `yaml:"requestAmount" json:"requestAmount"` + AwaitFeeFunding bool `yaml:"awaitFeeFunding" json:"awaitFeeFunding"` + + TxHashResultVar string `yaml:"txHashResultVar" json:"txHashResultVar"` +} + +func DefaultConfig() Config { + return Config{ + ShadowForkVaultContract: "0x9620e3933dAAa49EBe3250b731291ac817E24372", + + MinBalance: big.NewInt(1000000000000000000), // 1 ETH + TxFeeCap: big.NewInt(100000000000), // 100 Gwei + TxTipCap: big.NewInt(1000000000), // 1 Gwei + RequestAmount: big.NewInt(1000000000000000000), // 1 ETH + } +} + +func (c *Config) Validate() error { + if c.PrivateKey == "" { + return errors.New("privateKey must be set") + } + + return nil +} diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/BeaconChainProofs.sol b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/BeaconChainProofs.sol new file mode 100644 index 00000000..06bd9a5e --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/BeaconChainProofs.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +library BeaconChainProofs { + // constants are the number of fields and the heights of the different merkle trees used in merkleizing beacon chain containers + uint256 internal constant BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT = 3; + + uint256 internal constant BEACON_BLOCK_HEADER_FIELD_COUNT = 5; + + // in beacon block header https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader + uint256 internal constant SLOT_INDEX = 0; + + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + + /** + * @notice This function verifies the slot number against the block root. + * @param slotNumber is the beacon chain slot number to be proven against. + * @param proof is the provided merkle proof + * @param blockRoot is hashtree root of the latest block header in the beacon state + */ + function verifySlotAgainstBlockRoot( + bytes32 blockRoot, + uint256 slotNumber, + bytes memory proof + ) internal view returns (bool) { + if (proof.length != 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT)) { + return false; + } + + return Merkle.verifyInclusionSha256({ + proof: proof, + root: blockRoot, + leaf: bytes32(reverse(slotNumber)), + index: SLOT_INDEX + }); + } + + function generateBlockRootProof( + bytes32[] storage zeroHashes, + uint256 slotNumber, + uint256 proposerIndex, + bytes32 parentRoot, + bytes32 stateRoot, + bytes32 bodyRoot, + uint256 fieldIdx + ) internal view returns (bytes32, bytes memory) { + bytes32[] memory headerFieldRoots = new bytes32[](BEACON_BLOCK_HEADER_FIELD_COUNT); + + headerFieldRoots[0] = bytes32(reverse(slotNumber)); + headerFieldRoots[1] = bytes32(reverse(proposerIndex)); + headerFieldRoots[2] = parentRoot; + headerFieldRoots[3] = stateRoot; + headerFieldRoots[4] = bodyRoot; + + bytes32[][] memory tree = buildHashTree(zeroHashes, headerFieldRoots, BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT); + + bytes memory proof = buildProofFromTree(zeroHashes, tree, BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, fieldIdx); + bytes32 root = buildRootFromTree(tree); + + return (root, proof); + } + + function buildHashTree( + bytes32[] storage zeroHashes, + bytes32[] memory values, + uint256 layers + ) internal view returns (bytes32[][] memory) { + bytes32[][] memory tree = new bytes32[][](layers + 1); + tree[0] = values; + + for(uint256 l = 0; l < layers; l++) { + uint256 layerSize = tree[l].length; + uint256 paddedLayerSize; + if (layerSize % 2 == 1) { + paddedLayerSize = layerSize + 1; + } else { + paddedLayerSize = layerSize; + } + + uint256 nextLevelSize = paddedLayerSize / 2; + bytes32[] memory nextValues = new bytes32[](nextLevelSize); + + for (uint256 i = 0; i < paddedLayerSize; i += 2) { + bytes32 leftHash = tree[l][i]; + bytes32 rightHash; + + if(i+1 >= layerSize) { + rightHash = zeroHashes[l]; + } else { + rightHash = tree[l][i+1]; + } + + nextValues[i/2] = sha256(abi.encodePacked(leftHash, rightHash)); + } + + tree[l+1] = nextValues; + } + + return tree; + } + + function buildProofFromTree( + bytes32[] memory zeroHashes, + bytes32[][] memory tree, + uint256 layers, + uint256 index + ) internal pure returns (bytes memory) { + bytes32[] memory proof = new bytes32[](layers); + for(uint256 l = 0; l < layers; l++) { + + uint256 layerIndex = (index / (2**l))^1; + if(layerIndex < tree[l].length) { + proof[l] = tree[l][layerIndex]; + } else { + proof[l] = zeroHashes[l]; + } + } + + bytes memory result = abi.encodePacked(proof[0]); + for(uint256 l = 1; l < layers; l++) { + result = bytes.concat(result, abi.encodePacked(proof[l])); + } + + return result; + } + + function buildRootFromTree( + bytes32[][] memory tree + ) internal pure returns (bytes32) { + uint256 treeSize = tree.length; + return tree[treeSize-1][0]; + } + + function reverse(uint256 input) internal pure returns (uint256 v) { + v = input; + + // swap bytes + v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + + // swap 2-byte long pairs + v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + + // swap 4-byte long pairs + v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + + // swap 8-byte long pairs + v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | + ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + + // swap 16-byte long pairs + v = (v >> 128) | (v << 128); + } + +} \ No newline at end of file diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/Merkle.sol b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/Merkle.sol new file mode 100644 index 00000000..ddb0bd4c --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/Merkle.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofKeccak: proof length should be a non-zero multiple of 32" + ); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { + revert(0, 0) + } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { + revert(0, 0) + } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + @param leaves the leaves of the merkle tree + @return The computed Merkle root of the tree. + @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/README.md b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/README.md new file mode 100644 index 00000000..a9f3fb6e --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/README.md @@ -0,0 +1,9 @@ +# ShadowForkVault Contract + +Compiler: `solc 0.8.24+commit.e11b9ed9` +Deployment Address: `0x9620e3933dAAa49EBe3250b731291ac817E24372` (Holesky & Sepolia) + +## Commands + +Abigen: +`abigen --abi=ShadowForkVault.abi.json --pkg=shadowvaultcontract --out=ShadowForkVault.go` diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.abi.json b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.abi.json new file mode 100644 index 00000000..74d60259 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.abi.json @@ -0,0 +1,177 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "genesisTime", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "balanceof", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "slotNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "proposerIndex", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "parentRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "bodyRoot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "fieldIndex", + "type": "uint256" + } + ], + "name": "generateHeaderProof", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + } + ], + "name": "getBeaconRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "slot", + "type": "uint256" + } + ], + "name": "getBeaconRootBySlot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGenesisTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "slotTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "slotNumber", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "shadowWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.go b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.go new file mode 100644 index 00000000..fad25730 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.go @@ -0,0 +1,399 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package shadowvaultcontract + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ShadowvaultcontractMetaData contains all meta data concerning the Shadowvaultcontract contract. +var ShadowvaultcontractMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"genesisTime\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"balanceof\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"slotNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"proposerIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"parentRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"stateRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"bodyRoot\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"fieldIndex\",\"type\":\"uint256\"}],\"name\":\"generateHeaderProof\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"time\",\"type\":\"uint256\"}],\"name\":\"getBeaconRoot\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"slot\",\"type\":\"uint256\"}],\"name\":\"getBeaconRootBySlot\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getGenesisTime\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"slotTime\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"slotNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"shadowWithdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", +} + +// ShadowvaultcontractABI is the input ABI used to generate the binding from. +// Deprecated: Use ShadowvaultcontractMetaData.ABI instead. +var ShadowvaultcontractABI = ShadowvaultcontractMetaData.ABI + +// Shadowvaultcontract is an auto generated Go binding around an Ethereum contract. +type Shadowvaultcontract struct { + ShadowvaultcontractCaller // Read-only binding to the contract + ShadowvaultcontractTransactor // Write-only binding to the contract + ShadowvaultcontractFilterer // Log filterer for contract events +} + +// ShadowvaultcontractCaller is an auto generated read-only Go binding around an Ethereum contract. +type ShadowvaultcontractCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ShadowvaultcontractTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ShadowvaultcontractTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ShadowvaultcontractFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ShadowvaultcontractFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ShadowvaultcontractSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ShadowvaultcontractSession struct { + Contract *Shadowvaultcontract // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ShadowvaultcontractCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ShadowvaultcontractCallerSession struct { + Contract *ShadowvaultcontractCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ShadowvaultcontractTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ShadowvaultcontractTransactorSession struct { + Contract *ShadowvaultcontractTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ShadowvaultcontractRaw is an auto generated low-level Go binding around an Ethereum contract. +type ShadowvaultcontractRaw struct { + Contract *Shadowvaultcontract // Generic contract binding to access the raw methods on +} + +// ShadowvaultcontractCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ShadowvaultcontractCallerRaw struct { + Contract *ShadowvaultcontractCaller // Generic read-only contract binding to access the raw methods on +} + +// ShadowvaultcontractTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ShadowvaultcontractTransactorRaw struct { + Contract *ShadowvaultcontractTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewShadowvaultcontract creates a new instance of Shadowvaultcontract, bound to a specific deployed contract. +func NewShadowvaultcontract(address common.Address, backend bind.ContractBackend) (*Shadowvaultcontract, error) { + contract, err := bindShadowvaultcontract(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Shadowvaultcontract{ShadowvaultcontractCaller: ShadowvaultcontractCaller{contract: contract}, ShadowvaultcontractTransactor: ShadowvaultcontractTransactor{contract: contract}, ShadowvaultcontractFilterer: ShadowvaultcontractFilterer{contract: contract}}, nil +} + +// NewShadowvaultcontractCaller creates a new read-only instance of Shadowvaultcontract, bound to a specific deployed contract. +func NewShadowvaultcontractCaller(address common.Address, caller bind.ContractCaller) (*ShadowvaultcontractCaller, error) { + contract, err := bindShadowvaultcontract(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ShadowvaultcontractCaller{contract: contract}, nil +} + +// NewShadowvaultcontractTransactor creates a new write-only instance of Shadowvaultcontract, bound to a specific deployed contract. +func NewShadowvaultcontractTransactor(address common.Address, transactor bind.ContractTransactor) (*ShadowvaultcontractTransactor, error) { + contract, err := bindShadowvaultcontract(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ShadowvaultcontractTransactor{contract: contract}, nil +} + +// NewShadowvaultcontractFilterer creates a new log filterer instance of Shadowvaultcontract, bound to a specific deployed contract. +func NewShadowvaultcontractFilterer(address common.Address, filterer bind.ContractFilterer) (*ShadowvaultcontractFilterer, error) { + contract, err := bindShadowvaultcontract(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ShadowvaultcontractFilterer{contract: contract}, nil +} + +// bindShadowvaultcontract binds a generic wrapper to an already deployed contract. +func bindShadowvaultcontract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ShadowvaultcontractMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Shadowvaultcontract *ShadowvaultcontractRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Shadowvaultcontract.Contract.ShadowvaultcontractCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Shadowvaultcontract *ShadowvaultcontractRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.ShadowvaultcontractTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Shadowvaultcontract *ShadowvaultcontractRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.ShadowvaultcontractTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Shadowvaultcontract *ShadowvaultcontractCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Shadowvaultcontract.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Shadowvaultcontract *ShadowvaultcontractTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Shadowvaultcontract *ShadowvaultcontractTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.contract.Transact(opts, method, params...) +} + +// Balanceof is a free data retrieval call binding the contract method 0x3d64125b. +// +// Solidity: function balanceof(address addr) view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractCaller) Balanceof(opts *bind.CallOpts, addr common.Address) (*big.Int, error) { + var out []interface{} + err := _Shadowvaultcontract.contract.Call(opts, &out, "balanceof", addr) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Balanceof is a free data retrieval call binding the contract method 0x3d64125b. +// +// Solidity: function balanceof(address addr) view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractSession) Balanceof(addr common.Address) (*big.Int, error) { + return _Shadowvaultcontract.Contract.Balanceof(&_Shadowvaultcontract.CallOpts, addr) +} + +// Balanceof is a free data retrieval call binding the contract method 0x3d64125b. +// +// Solidity: function balanceof(address addr) view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractCallerSession) Balanceof(addr common.Address) (*big.Int, error) { + return _Shadowvaultcontract.Contract.Balanceof(&_Shadowvaultcontract.CallOpts, addr) +} + +// GenerateHeaderProof is a free data retrieval call binding the contract method 0x68a6ad99. +// +// Solidity: function generateHeaderProof(uint256 slotNumber, uint256 proposerIndex, bytes32 parentRoot, bytes32 stateRoot, bytes32 bodyRoot, uint256 fieldIndex) view returns(bytes) +func (_Shadowvaultcontract *ShadowvaultcontractCaller) GenerateHeaderProof(opts *bind.CallOpts, slotNumber *big.Int, proposerIndex *big.Int, parentRoot [32]byte, stateRoot [32]byte, bodyRoot [32]byte, fieldIndex *big.Int) ([]byte, error) { + var out []interface{} + err := _Shadowvaultcontract.contract.Call(opts, &out, "generateHeaderProof", slotNumber, proposerIndex, parentRoot, stateRoot, bodyRoot, fieldIndex) + + if err != nil { + return *new([]byte), err + } + + out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) + + return out0, err + +} + +// GenerateHeaderProof is a free data retrieval call binding the contract method 0x68a6ad99. +// +// Solidity: function generateHeaderProof(uint256 slotNumber, uint256 proposerIndex, bytes32 parentRoot, bytes32 stateRoot, bytes32 bodyRoot, uint256 fieldIndex) view returns(bytes) +func (_Shadowvaultcontract *ShadowvaultcontractSession) GenerateHeaderProof(slotNumber *big.Int, proposerIndex *big.Int, parentRoot [32]byte, stateRoot [32]byte, bodyRoot [32]byte, fieldIndex *big.Int) ([]byte, error) { + return _Shadowvaultcontract.Contract.GenerateHeaderProof(&_Shadowvaultcontract.CallOpts, slotNumber, proposerIndex, parentRoot, stateRoot, bodyRoot, fieldIndex) +} + +// GenerateHeaderProof is a free data retrieval call binding the contract method 0x68a6ad99. +// +// Solidity: function generateHeaderProof(uint256 slotNumber, uint256 proposerIndex, bytes32 parentRoot, bytes32 stateRoot, bytes32 bodyRoot, uint256 fieldIndex) view returns(bytes) +func (_Shadowvaultcontract *ShadowvaultcontractCallerSession) GenerateHeaderProof(slotNumber *big.Int, proposerIndex *big.Int, parentRoot [32]byte, stateRoot [32]byte, bodyRoot [32]byte, fieldIndex *big.Int) ([]byte, error) { + return _Shadowvaultcontract.Contract.GenerateHeaderProof(&_Shadowvaultcontract.CallOpts, slotNumber, proposerIndex, parentRoot, stateRoot, bodyRoot, fieldIndex) +} + +// GetBeaconRoot is a free data retrieval call binding the contract method 0x661a052f. +// +// Solidity: function getBeaconRoot(uint256 time) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractCaller) GetBeaconRoot(opts *bind.CallOpts, time *big.Int) ([32]byte, error) { + var out []interface{} + err := _Shadowvaultcontract.contract.Call(opts, &out, "getBeaconRoot", time) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetBeaconRoot is a free data retrieval call binding the contract method 0x661a052f. +// +// Solidity: function getBeaconRoot(uint256 time) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractSession) GetBeaconRoot(time *big.Int) ([32]byte, error) { + return _Shadowvaultcontract.Contract.GetBeaconRoot(&_Shadowvaultcontract.CallOpts, time) +} + +// GetBeaconRoot is a free data retrieval call binding the contract method 0x661a052f. +// +// Solidity: function getBeaconRoot(uint256 time) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractCallerSession) GetBeaconRoot(time *big.Int) ([32]byte, error) { + return _Shadowvaultcontract.Contract.GetBeaconRoot(&_Shadowvaultcontract.CallOpts, time) +} + +// GetBeaconRootBySlot is a free data retrieval call binding the contract method 0x692d1ddf. +// +// Solidity: function getBeaconRootBySlot(uint256 slot) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractCaller) GetBeaconRootBySlot(opts *bind.CallOpts, slot *big.Int) ([32]byte, error) { + var out []interface{} + err := _Shadowvaultcontract.contract.Call(opts, &out, "getBeaconRootBySlot", slot) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetBeaconRootBySlot is a free data retrieval call binding the contract method 0x692d1ddf. +// +// Solidity: function getBeaconRootBySlot(uint256 slot) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractSession) GetBeaconRootBySlot(slot *big.Int) ([32]byte, error) { + return _Shadowvaultcontract.Contract.GetBeaconRootBySlot(&_Shadowvaultcontract.CallOpts, slot) +} + +// GetBeaconRootBySlot is a free data retrieval call binding the contract method 0x692d1ddf. +// +// Solidity: function getBeaconRootBySlot(uint256 slot) view returns(bytes32) +func (_Shadowvaultcontract *ShadowvaultcontractCallerSession) GetBeaconRootBySlot(slot *big.Int) ([32]byte, error) { + return _Shadowvaultcontract.Contract.GetBeaconRootBySlot(&_Shadowvaultcontract.CallOpts, slot) +} + +// GetGenesisTime is a free data retrieval call binding the contract method 0x723d8e96. +// +// Solidity: function getGenesisTime() view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractCaller) GetGenesisTime(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Shadowvaultcontract.contract.Call(opts, &out, "getGenesisTime") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetGenesisTime is a free data retrieval call binding the contract method 0x723d8e96. +// +// Solidity: function getGenesisTime() view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractSession) GetGenesisTime() (*big.Int, error) { + return _Shadowvaultcontract.Contract.GetGenesisTime(&_Shadowvaultcontract.CallOpts) +} + +// GetGenesisTime is a free data retrieval call binding the contract method 0x723d8e96. +// +// Solidity: function getGenesisTime() view returns(uint256) +func (_Shadowvaultcontract *ShadowvaultcontractCallerSession) GetGenesisTime() (*big.Int, error) { + return _Shadowvaultcontract.Contract.GetGenesisTime(&_Shadowvaultcontract.CallOpts) +} + +// ShadowWithdraw is a paid mutator transaction binding the contract method 0x7c340fcf. +// +// Solidity: function shadowWithdraw(uint256 slotTime, uint256 slotNumber, bytes proof, address target, uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactor) ShadowWithdraw(opts *bind.TransactOpts, slotTime *big.Int, slotNumber *big.Int, proof []byte, target common.Address, amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.contract.Transact(opts, "shadowWithdraw", slotTime, slotNumber, proof, target, amount) +} + +// ShadowWithdraw is a paid mutator transaction binding the contract method 0x7c340fcf. +// +// Solidity: function shadowWithdraw(uint256 slotTime, uint256 slotNumber, bytes proof, address target, uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractSession) ShadowWithdraw(slotTime *big.Int, slotNumber *big.Int, proof []byte, target common.Address, amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.ShadowWithdraw(&_Shadowvaultcontract.TransactOpts, slotTime, slotNumber, proof, target, amount) +} + +// ShadowWithdraw is a paid mutator transaction binding the contract method 0x7c340fcf. +// +// Solidity: function shadowWithdraw(uint256 slotTime, uint256 slotNumber, bytes proof, address target, uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactorSession) ShadowWithdraw(slotTime *big.Int, slotNumber *big.Int, proof []byte, target common.Address, amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.ShadowWithdraw(&_Shadowvaultcontract.TransactOpts, slotTime, slotNumber, proof, target, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactor) Withdraw(opts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.contract.Transact(opts, "withdraw", amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractSession) Withdraw(amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.Withdraw(&_Shadowvaultcontract.TransactOpts, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactorSession) Withdraw(amount *big.Int) (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.Withdraw(&_Shadowvaultcontract.TransactOpts, amount) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Shadowvaultcontract.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Shadowvaultcontract *ShadowvaultcontractSession) Receive() (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.Receive(&_Shadowvaultcontract.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Shadowvaultcontract *ShadowvaultcontractTransactorSession) Receive() (*types.Transaction, error) { + return _Shadowvaultcontract.Contract.Receive(&_Shadowvaultcontract.TransactOpts) +} diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.input.json b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.input.json new file mode 100644 index 00000000..64c22137 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.input.json @@ -0,0 +1,41 @@ +{ + "language": "Solidity", + "sources": { + "contracts/ShadowForkVault.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.0;\n\nimport \"./BeaconChainProofs.sol\";\n\ncontract ShadowForkVault {\n uint256 private originGenesisTime;\n bytes32[] private zeroHashes;\n mapping(address => uint256) private depositBalance;\n\n\n constructor(uint256 genesisTime) {\n originGenesisTime = genesisTime;\n\n zeroHashes = new bytes32[](33);\n for(uint256 i = 0; i < 32; i++) {\n bytes32 zeroHash = zeroHashes[i];\n zeroHashes[i+1] = sha256(abi.encodePacked(zeroHash, zeroHash));\n }\n }\n\n function getGenesisTime() public view returns (uint256) {\n return originGenesisTime;\n }\n\n function balanceof(address addr) public view returns (uint256) {\n return depositBalance[addr];\n }\n\n function getBeaconRootBySlot(uint256 slot) public view returns (bytes32) {\n uint256 slotTime = (slot * BeaconChainProofs.SECONDS_PER_SLOT) + originGenesisTime;\n return getBeaconRoot(slotTime);\n }\n\n function getBeaconRoot(uint256 time) public view returns (bytes32) {\n bytes32 result;\n (bool isSuccess, bytes memory response) = address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02).staticcall(abi.encodePacked(time + BeaconChainProofs.SECONDS_PER_SLOT));\n if(isSuccess) {\n assembly {\n result := mload(add(response, 32))\n }\n }\n return result;\n }\n\n // helper function to generate proofs\n // use fieldIndex = 0 for slot number proofs\n function generateHeaderProof(\n uint256 slotNumber,\n uint256 proposerIndex,\n bytes32 parentRoot,\n bytes32 stateRoot,\n bytes32 bodyRoot,\n uint256 fieldIndex\n ) public view returns (bytes memory) {\n (, bytes memory proof) = BeaconChainProofs.generateBlockRootProof(\n zeroHashes,\n slotNumber,\n proposerIndex,\n parentRoot,\n stateRoot,\n bodyRoot,\n fieldIndex\n );\n return proof;\n }\n\n receive() external payable {\n depositBalance[msg.sender] += msg.value;\n }\n\n function withdraw(uint256 amount) public {\n require(amount > 0, \"amount must be greater than 0\");\n require(depositBalance[msg.sender] >= amount, \"amount exceeds balance\");\n\n depositBalance[msg.sender] -= amount;\n\n (bool sent, ) = payable(msg.sender).call{value: amount}(\"\");\n require(sent, \"failed to send ether\");\n }\n\n function shadowWithdraw(\n uint256 slotTime,\n uint256 slotNumber,\n bytes memory proof,\n address target,\n uint256 amount\n ) public {\n bytes32 blockRoot = getBeaconRoot(slotTime);\n require(blockRoot != bytes32(0), \"no block root for slot time\");\n\n bool proofValidity = BeaconChainProofs.verifySlotAgainstBlockRoot(blockRoot, slotNumber, proof);\n require(proofValidity, \"block root verification failed\");\n\n uint256 currentGenesisTime = slotTime - (slotNumber * BeaconChainProofs.SECONDS_PER_SLOT);\n require(currentGenesisTime > originGenesisTime, \"not a shadow fork\");\n\n require(address(this).balance >= amount, \"amount exceeds balance\");\n if (amount == 0) {\n amount = address(this).balance;\n }\n\n (bool sent, ) = payable(target).call{value: amount}(\"\");\n require(sent, \"failed to send ether\");\n }\n}" + }, + "contracts/BeaconChainProofs.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\n\npragma solidity ^0.8.0;\n\nimport \"./Merkle.sol\";\n\n//Utility library for parsing and PHASE0 beacon chain block headers\n//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization\n//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader\nlibrary BeaconChainProofs {\n // constants are the number of fields and the heights of the different merkle trees used in merkleizing beacon chain containers\n uint256 internal constant BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT = 3;\n\n uint256 internal constant BEACON_BLOCK_HEADER_FIELD_COUNT = 5;\n\n // in beacon block header https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader\n uint256 internal constant SLOT_INDEX = 0;\n\n /// @notice The number of seconds in a slot in the beacon chain\n uint64 internal constant SECONDS_PER_SLOT = 12;\n\n /**\n * @notice This function verifies the slot number against the block root.\n * @param slotNumber is the beacon chain slot number to be proven against.\n * @param proof is the provided merkle proof\n * @param blockRoot is hashtree root of the latest block header in the beacon state\n */\n function verifySlotAgainstBlockRoot(\n bytes32 blockRoot,\n uint256 slotNumber,\n bytes memory proof\n ) internal view returns (bool) {\n if (proof.length != 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT)) {\n return false;\n }\n\n return Merkle.verifyInclusionSha256({\n proof: proof,\n root: blockRoot,\n leaf: bytes32(reverse(slotNumber)),\n index: SLOT_INDEX\n });\n }\n\n function generateBlockRootProof(\n bytes32[] storage zeroHashes,\n uint256 slotNumber,\n uint256 proposerIndex,\n bytes32 parentRoot,\n bytes32 stateRoot,\n bytes32 bodyRoot,\n uint256 fieldIdx\n ) internal view returns (bytes32, bytes memory) {\n bytes32[] memory headerFieldRoots = new bytes32[](BEACON_BLOCK_HEADER_FIELD_COUNT);\n\n headerFieldRoots[0] = bytes32(reverse(slotNumber));\n headerFieldRoots[1] = bytes32(reverse(proposerIndex));\n headerFieldRoots[2] = parentRoot;\n headerFieldRoots[3] = stateRoot;\n headerFieldRoots[4] = bodyRoot;\n\n bytes32[][] memory tree = buildHashTree(zeroHashes, headerFieldRoots, BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT);\n\n bytes memory proof = buildProofFromTree(zeroHashes, tree, BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, fieldIdx);\n bytes32 root = buildRootFromTree(tree);\n\n return (root, proof);\n }\n\n function buildHashTree(\n bytes32[] storage zeroHashes,\n bytes32[] memory values,\n uint256 layers\n ) internal view returns (bytes32[][] memory) {\n bytes32[][] memory tree = new bytes32[][](layers + 1);\n tree[0] = values;\n\n for(uint256 l = 0; l < layers; l++) {\n uint256 layerSize = tree[l].length;\n uint256 paddedLayerSize;\n if (layerSize % 2 == 1) {\n paddedLayerSize = layerSize + 1;\n } else {\n paddedLayerSize = layerSize;\n }\n\n uint256 nextLevelSize = paddedLayerSize / 2;\n bytes32[] memory nextValues = new bytes32[](nextLevelSize);\n\n for (uint256 i = 0; i < paddedLayerSize; i += 2) {\n bytes32 leftHash = tree[l][i];\n bytes32 rightHash;\n \n if(i+1 >= layerSize) {\n rightHash = zeroHashes[l];\n } else {\n rightHash = tree[l][i+1];\n }\n\n nextValues[i/2] = sha256(abi.encodePacked(leftHash, rightHash));\n }\n\n tree[l+1] = nextValues;\n }\n\n return tree;\n }\n\n function buildProofFromTree(\n bytes32[] memory zeroHashes,\n bytes32[][] memory tree,\n uint256 layers,\n uint256 index\n ) internal pure returns (bytes memory) {\n bytes32[] memory proof = new bytes32[](layers);\n for(uint256 l = 0; l < layers; l++) {\n\n uint256 layerIndex = (index / (2**l))^1;\n if(layerIndex < tree[l].length) {\n proof[l] = tree[l][layerIndex];\n } else {\n proof[l] = zeroHashes[l];\n }\n }\n\n bytes memory result = abi.encodePacked(proof[0]);\n for(uint256 l = 1; l < layers; l++) {\n result = bytes.concat(result, abi.encodePacked(proof[l]));\n }\n \n return result;\n }\n\n function buildRootFromTree(\n bytes32[][] memory tree\n ) internal pure returns (bytes32) {\n uint256 treeSize = tree.length;\n return tree[treeSize-1][0];\n }\n\n function reverse(uint256 input) internal pure returns (uint256 v) {\n v = input;\n\n // swap bytes\n v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) |\n ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8);\n\n // swap 2-byte long pairs\n v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) |\n ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16);\n\n // swap 4-byte long pairs\n v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) |\n ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32);\n\n // swap 8-byte long pairs\n v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) |\n ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64);\n\n // swap 16-byte long pairs\n v = (v >> 128) | (v << 128);\n }\n\n}" + }, + "contracts/Merkle.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev These functions deal with verification of Merkle Tree proofs.\n *\n * The tree and the proofs can be generated using our\n * https://github.com/OpenZeppelin/merkle-tree[JavaScript library].\n * You will find a quickstart guide in the readme.\n *\n * WARNING: You should avoid using leaf values that are 64 bytes long prior to\n * hashing, or use a hash function other than keccak256 for hashing leaves.\n * This is because the concatenation of a sorted pair of internal nodes in\n * the merkle tree could be reinterpreted as a leaf value.\n * OpenZeppelin's JavaScript library generates merkle trees that are safe\n * against this attack out of the box.\n */\nlibrary Merkle {\n /**\n * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up\n * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt\n * hash matches the root of the tree. The tree is built assuming `leaf` is\n * the 0 indexed `index`'th leaf from the bottom left of the tree.\n *\n * Note this is for a Merkle tree using the keccak/sha3 hash function\n */\n function verifyInclusionKeccak(\n bytes memory proof,\n bytes32 root,\n bytes32 leaf,\n uint256 index\n ) internal pure returns (bool) {\n return processInclusionProofKeccak(proof, leaf, index) == root;\n }\n\n /**\n * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up\n * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt\n * hash matches the root of the tree. The tree is built assuming `leaf` is\n * the 0 indexed `index`'th leaf from the bottom left of the tree.\n *\n * _Available since v4.4._\n *\n * Note this is for a Merkle tree using the keccak/sha3 hash function\n */\n function processInclusionProofKeccak(\n bytes memory proof,\n bytes32 leaf,\n uint256 index\n ) internal pure returns (bytes32) {\n require(\n proof.length != 0 && proof.length % 32 == 0,\n \"Merkle.processInclusionProofKeccak: proof length should be a non-zero multiple of 32\"\n );\n bytes32 computedHash = leaf;\n for (uint256 i = 32; i <= proof.length; i += 32) {\n if (index % 2 == 0) {\n // if ith bit of index is 0, then computedHash is a left sibling\n assembly {\n mstore(0x00, computedHash)\n mstore(0x20, mload(add(proof, i)))\n computedHash := keccak256(0x00, 0x40)\n index := div(index, 2)\n }\n } else {\n // if ith bit of index is 1, then computedHash is a right sibling\n assembly {\n mstore(0x00, mload(add(proof, i)))\n mstore(0x20, computedHash)\n computedHash := keccak256(0x00, 0x40)\n index := div(index, 2)\n }\n }\n }\n return computedHash;\n }\n\n /**\n * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up\n * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt\n * hash matches the root of the tree. The tree is built assuming `leaf` is\n * the 0 indexed `index`'th leaf from the bottom left of the tree.\n *\n * Note this is for a Merkle tree using the sha256 hash function\n */\n function verifyInclusionSha256(\n bytes memory proof,\n bytes32 root,\n bytes32 leaf,\n uint256 index\n ) internal view returns (bool) {\n return processInclusionProofSha256(proof, leaf, index) == root;\n }\n\n /**\n * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up\n * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt\n * hash matches the root of the tree. The tree is built assuming `leaf` is\n * the 0 indexed `index`'th leaf from the bottom left of the tree.\n *\n * _Available since v4.4._\n *\n * Note this is for a Merkle tree using the sha256 hash function\n */\n function processInclusionProofSha256(\n bytes memory proof,\n bytes32 leaf,\n uint256 index\n ) internal view returns (bytes32) {\n require(\n proof.length != 0 && proof.length % 32 == 0,\n \"Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32\"\n );\n bytes32[1] memory computedHash = [leaf];\n for (uint256 i = 32; i <= proof.length; i += 32) {\n if (index % 2 == 0) {\n // if ith bit of index is 0, then computedHash is a left sibling\n assembly {\n mstore(0x00, mload(computedHash))\n mstore(0x20, mload(add(proof, i)))\n if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) {\n revert(0, 0)\n }\n index := div(index, 2)\n }\n } else {\n // if ith bit of index is 1, then computedHash is a right sibling\n assembly {\n mstore(0x00, mload(add(proof, i)))\n mstore(0x20, mload(computedHash))\n if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) {\n revert(0, 0)\n }\n index := div(index, 2)\n }\n }\n }\n return computedHash[0];\n }\n\n /**\n @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function\n @param leaves the leaves of the merkle tree\n @return The computed Merkle root of the tree.\n @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly.\n */\n function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) {\n //there are half as many nodes in the layer above the leaves\n uint256 numNodesInLayer = leaves.length / 2;\n //create a layer to store the internal nodes\n bytes32[] memory layer = new bytes32[](numNodesInLayer);\n //fill the layer with the pairwise hashes of the leaves\n for (uint256 i = 0; i < numNodesInLayer; i++) {\n layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1]));\n }\n //the next layer above has half as many nodes\n numNodesInLayer /= 2;\n //while we haven't computed the root\n while (numNodesInLayer != 0) {\n //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children\n for (uint256 i = 0; i < numNodesInLayer; i++) {\n layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));\n }\n //the next layer above has half as many nodes\n numNodesInLayer /= 2;\n }\n //the first node in the layer is the root\n return layer[0];\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 2000 + }, + "outputSelection": { + "*": { + "": [ + "ast" + ], + "*": [ + "abi", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.legacyAssembly", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "evm.gasEstimates", + "evm.assembly" + ] + } + }, + "remappings": [] + } +} \ No newline at end of file diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.sol b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.sol new file mode 100644 index 00000000..cf889ff5 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract/ShadowForkVault.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "./BeaconChainProofs.sol"; + +contract ShadowForkVault { + uint256 private originGenesisTime; + bytes32[] private zeroHashes; + mapping(address => uint256) private depositBalance; + + + constructor(uint256 genesisTime) { + originGenesisTime = genesisTime; + + zeroHashes = new bytes32[](33); + for(uint256 i = 0; i < 32; i++) { + bytes32 zeroHash = zeroHashes[i]; + zeroHashes[i+1] = sha256(abi.encodePacked(zeroHash, zeroHash)); + } + } + + function getGenesisTime() public view returns (uint256) { + return originGenesisTime; + } + + function balanceof(address addr) public view returns (uint256) { + return depositBalance[addr]; + } + + function getBeaconRootBySlot(uint256 slot) public view returns (bytes32) { + uint256 slotTime = (slot * BeaconChainProofs.SECONDS_PER_SLOT) + originGenesisTime; + return getBeaconRoot(slotTime); + } + + function getBeaconRoot(uint256 time) public view returns (bytes32) { + bytes32 result; + (bool isSuccess, bytes memory response) = address(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02).staticcall(abi.encodePacked(time + BeaconChainProofs.SECONDS_PER_SLOT)); + if(isSuccess) { + assembly { + result := mload(add(response, 32)) + } + } + return result; + } + + // helper function to generate proofs + // use fieldIndex = 0 for slot number proofs + function generateHeaderProof( + uint256 slotNumber, + uint256 proposerIndex, + bytes32 parentRoot, + bytes32 stateRoot, + bytes32 bodyRoot, + uint256 fieldIndex + ) public view returns (bytes memory) { + (, bytes memory proof) = BeaconChainProofs.generateBlockRootProof( + zeroHashes, + slotNumber, + proposerIndex, + parentRoot, + stateRoot, + bodyRoot, + fieldIndex + ); + return proof; + } + + receive() external payable { + depositBalance[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) public { + require(amount > 0, "amount must be greater than 0"); + require(depositBalance[msg.sender] >= amount, "amount exceeds balance"); + + depositBalance[msg.sender] -= amount; + + (bool sent, ) = payable(msg.sender).call{value: amount}(""); + require(sent, "failed to send ether"); + } + + function shadowWithdraw( + uint256 slotTime, + uint256 slotNumber, + bytes memory proof, + address target, + uint256 amount + ) public { + bytes32 blockRoot = getBeaconRoot(slotTime); + require(blockRoot != bytes32(0), "no block root for slot time"); + + bool proofValidity = BeaconChainProofs.verifySlotAgainstBlockRoot(blockRoot, slotNumber, proof); + require(proofValidity, "block root verification failed"); + + uint256 currentGenesisTime = slotTime - (slotNumber * BeaconChainProofs.SECONDS_PER_SLOT); + require(currentGenesisTime > originGenesisTime, "not a shadow fork"); + + require(address(this).balance >= amount, "amount exceeds balance"); + if (amount == 0) { + amount = address(this).balance; + } + + (bool sent, ) = payable(target).call{value: amount}(""); + require(sent, "failed to send ether"); + } +} \ No newline at end of file diff --git a/pkg/coordinator/tasks/generate_shadowfork_funding/task.go b/pkg/coordinator/tasks/generate_shadowfork_funding/task.go new file mode 100644 index 00000000..9380ee26 --- /dev/null +++ b/pkg/coordinator/tasks/generate_shadowfork_funding/task.go @@ -0,0 +1,329 @@ +package generateshadowforkfunding + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethpandaops/assertoor/pkg/coordinator/clients/consensus" + "github.com/ethpandaops/assertoor/pkg/coordinator/clients/execution" + shadowvaultcontract "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_shadowfork_funding/shadowvault_contract" + "github.com/ethpandaops/assertoor/pkg/coordinator/types" + "github.com/ethpandaops/assertoor/pkg/coordinator/wallet" + "github.com/sirupsen/logrus" +) + +var ( + TaskName = "generate_shadowfork_funding" + TaskDescriptor = &types.TaskDescriptor{ + Name: TaskName, + Description: "Generates a transaction that requests funds from the ShadowForkVault on shadow forks.", + Config: DefaultConfig(), + NewTask: NewTask, + } +) + +const shadowWithdrawGasLimit = 50000 + +type Task struct { + ctx *types.TaskContext + options *types.TaskOptions + config Config + logger logrus.FieldLogger + wallet *wallet.Wallet + vaultAddr common.Address +} + +func NewTask(ctx *types.TaskContext, options *types.TaskOptions) (types.Task, error) { + return &Task{ + ctx: ctx, + options: options, + logger: ctx.Logger.GetLogger(), + }, nil +} + +func (t *Task) Name() string { + return TaskDescriptor.Name +} + +func (t *Task) Title() string { + return t.ctx.Vars.ResolvePlaceholders(t.options.Title) +} + +func (t *Task) Description() string { + return TaskDescriptor.Description +} + +func (t *Task) Config() interface{} { + return t.config +} + +func (t *Task) Logger() logrus.FieldLogger { + return t.logger +} + +func (t *Task) Timeout() time.Duration { + return t.options.Timeout.Duration +} + +func (t *Task) LoadConfig() error { + config := DefaultConfig() + + // parse static config + if t.options.Config != nil { + if err := t.options.Config.Unmarshal(&config); err != nil { + return fmt.Errorf("error parsing task config for %v: %w", TaskName, err) + } + } + + // load dynamic vars + err := t.ctx.Vars.ConsumeVars(&config, t.options.ConfigVars) + if err != nil { + return err + } + + // validate config + if err2 := config.Validate(); err2 != nil { + return err2 + } + + // load wallets + privKey, err := crypto.HexToECDSA(config.PrivateKey) + if err != nil { + return err + } + + t.wallet, err = t.ctx.Scheduler.GetServices().WalletManager().GetWalletByPrivkey(privKey) + if err != nil { + return fmt.Errorf("cannot initialize wallet: %w", err) + } + + // parse vault addr + if config.ShadowForkVaultContract != "" { + err = t.vaultAddr.UnmarshalText([]byte(config.ShadowForkVaultContract)) + if err != nil { + return fmt.Errorf("cannot decode execution addr: %w", err) + } + } + + t.config = config + + return nil +} + +func (t *Task) Execute(ctx context.Context) error { + err := t.wallet.AwaitReady(ctx) + if err != nil { + return err + } + + t.logger.Infof("root wallet: %v [nonce: %v] %v ETH", t.wallet.GetAddress().Hex(), t.wallet.GetNonce(), t.wallet.GetReadableBalance(18, 0, 4, false, false)) + + if t.wallet.GetBalance().Cmp(t.config.MinBalance) >= 0 { + t.logger.Infof("balance (%v ETH) exceeds minBalance (%v ETH), skipping shadow vault request", t.wallet.GetReadableBalance(18, 0, 4, false, false), wallet.GetReadableBalance(t.config.MinBalance, 18, 0, 4, false, false)) + return nil + } + + clientPool := t.ctx.Scheduler.GetServices().ClientPool() + + client := clientPool.GetExecutionPool().AwaitReadyEndpoint(ctx, execution.AnyClient) + if client == nil { + return ctx.Err() + } + + vaultContract, err := shadowvaultcontract.NewShadowvaultcontract(t.vaultAddr, client.GetRPCClient().GetEthClient()) + if err != nil { + return fmt.Errorf("cannot create bound instance of ShadowVaultContract: %w", err) + } + + // check shadow fork eligibility + vaultGenesisTimeBigInt, err := vaultContract.GetGenesisTime(&bind.CallOpts{ + Context: ctx, + }) + if err != nil { + return fmt.Errorf("error requesting genesis time from ShadowVaultContract: %w", err) + } + + vaultGenesisTime := vaultGenesisTimeBigInt.Int64() + + consensusPool := clientPool.GetConsensusPool() + consensusPool.AwaitReadyEndpoint(ctx, consensus.AnyClient) + consensusGenesis := consensusPool.GetBlockCache().GetGenesis() + + if vaultGenesisTime >= consensusGenesis.GenesisTime.Unix() { + return fmt.Errorf("cannot request funds from ShadowForkVault when not on a shadow fork") + } + + wallclockSubscription := consensusPool.GetBlockCache().SubscribeWallclockSlotEvent(1) + defer wallclockSubscription.Unsubscribe() + + // generate shadow withdrawal tx + var withdrawalTx *ethtypes.Transaction + + minFeeBalance := big.NewInt(shadowWithdrawGasLimit) + minFeeBalance = minFeeBalance.Mul(minFeeBalance, t.config.TxFeeCap) + + retry := 0 + + for { + retry++ + if retry > 1 { + select { + case <-ctx.Done(): + return ctx.Err() + case <-wallclockSubscription.Channel(): + } + } + + client := clientPool.GetExecutionPool().AwaitReadyEndpoint(ctx, execution.AnyClient) + if client == nil { + continue + } + + balance, err := client.GetRPCClient().GetBalanceAt(ctx, t.wallet.GetAddress(), nil) + if err != nil { + continue + } + + if balance.Cmp(minFeeBalance) < 0 { + t.logger.Infof("not enough funds to pay withdrawal tx fee (have %v, need: %v)", wallet.GetReadableBalance(balance, 18, 0, 4, false, false), wallet.GetReadableBalance(minFeeBalance, 18, 0, 4, false, false)) + continue + } + + if withdrawalTx == nil { + tx, err2 := t.generateShadowWithdrawTx(ctx) + if err2 != nil { + t.logger.Infof("could not generate withdrawal: %v", err2) + continue + } + + withdrawalTx = tx + } + + err = client.GetRPCClient().SendTransaction(ctx, withdrawalTx) + if err != nil { + t.logger.Infof("failed sending shadow withdraw transaction: %w", err) + continue + } + + break + } + + if withdrawalTx != nil { + // await confirmation + receipt, err := t.wallet.AwaitTransaction(ctx, withdrawalTx) + + if err != nil { + return err + } + + t.logger.Infof("shadow withdraw tx confirmed! status: %v", receipt.Status) + t.wallet.ResyncState() + } + + return ctx.Err() +} + +func (t *Task) generateShadowWithdrawTx(ctx context.Context) (*ethtypes.Transaction, error) { + clientPool := t.ctx.Scheduler.GetServices().ClientPool() + consensusPool := clientPool.GetConsensusPool() + consensusGenesis := consensusPool.GetBlockCache().GetGenesis() + + if consensusGenesis.GenesisTime.Compare(time.Now()) >= 0 { + return nil, fmt.Errorf("before genesis time") + } + + currentSlot, _, err := consensusPool.GetBlockCache().GetWallclock().Now() + if err != nil { + return nil, fmt.Errorf("error reading wall clock: %v", err) + } + + if currentSlot.Number() < 5 { + return nil, fmt.Errorf("before slot 5 (slot: %v)", currentSlot.Number()) + } + + spec := consensusPool.GetBlockCache().GetSpecs() + if spec == nil { + return nil, fmt.Errorf("could not get spec") + } + + // generate header proof for slot N-1 + elClient := clientPool.GetExecutionPool().AwaitReadyEndpoint(ctx, execution.AnyClient) + if elClient == nil { + return nil, ctx.Err() + } + + clClient := clientPool.GetAllClients()[elClient.GetIndex()].ConsensusClient + _, clHeadRoot := clClient.GetLastHead() + + clBlock := consensusPool.GetBlockCache().GetCachedBlockByRoot(clHeadRoot) + if clBlock == nil { + return nil, fmt.Errorf("could not get head block") + } + + clParentRoot := clBlock.GetParentRoot() + if clParentRoot == nil { + return nil, fmt.Errorf("could not get parent root") + } + + clBlock = consensusPool.GetBlockCache().GetCachedBlockByRoot(*clParentRoot) + if clBlock == nil { + return nil, fmt.Errorf("could not get parent block") + } + + clBlockHead := clBlock.AwaitHeader(ctx, 1*time.Second) + if clBlockHead == nil { + return nil, fmt.Errorf("could not get parent block header") + } + + vaultContract, err := shadowvaultcontract.NewShadowvaultcontract(t.vaultAddr, elClient.GetRPCClient().GetEthClient()) + if err != nil { + return nil, fmt.Errorf("cannot create bound instance of ShadowVaultContract: %w", err) + } + + proof, err := vaultContract.GenerateHeaderProof( + &bind.CallOpts{ + Context: ctx, + }, + big.NewInt(int64(clBlockHead.Message.Slot)), + big.NewInt(int64(clBlockHead.Message.ProposerIndex)), + clBlockHead.Message.ParentRoot, + clBlockHead.Message.StateRoot, + clBlockHead.Message.BodyRoot, + big.NewInt(0), + ) + if err != nil { + return nil, fmt.Errorf("error while creating header proof: %w", err) + } + + // create request transaction + tx, err := t.wallet.BuildTransaction(ctx, func(_ context.Context, nonce uint64, signer bind.SignerFn) (*ethtypes.Transaction, error) { + return vaultContract.ShadowWithdraw( + &bind.TransactOpts{ + From: t.wallet.GetAddress(), + Nonce: big.NewInt(int64(nonce)), + Value: big.NewInt(0), + GasLimit: 50000, + GasFeeCap: t.config.TxFeeCap, + GasTipCap: t.config.TxTipCap, + Signer: signer, + NoSend: true, + }, + big.NewInt(consensusGenesis.GenesisTime.Unix()+(int64(clBlockHead.Message.Slot)*int64(spec.SecondsPerSlot))), + big.NewInt(int64(clBlockHead.Message.Slot)), + proof, + t.wallet.GetAddress(), + t.config.RequestAmount, + ) + }) + if err != nil { + return nil, fmt.Errorf("cannot build shadow withdraw transaction: %w", err) + } + + return tx, nil +} diff --git a/pkg/coordinator/tasks/tasks.go b/pkg/coordinator/tasks/tasks.go index ba0add04..67a8218f 100644 --- a/pkg/coordinator/tasks/tasks.go +++ b/pkg/coordinator/tasks/tasks.go @@ -21,6 +21,7 @@ import ( generateeoatransactions "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_eoa_transactions" generateexits "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_exits" generaterandommnemonic "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_random_mnemonic" + generateshadowforkfunding "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_shadowfork_funding" generateslashings "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_slashings" generatetransaction "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/generate_transaction" runcommand "github.com/ethpandaops/assertoor/pkg/coordinator/tasks/run_command" @@ -52,6 +53,7 @@ var AvailableTaskDescriptors = []*types.TaskDescriptor{ generatedeposits.TaskDescriptor, generateexits.TaskDescriptor, generaterandommnemonic.TaskDescriptor, + generateshadowforkfunding.TaskDescriptor, generateslashings.TaskDescriptor, generatetransaction.TaskDescriptor, runcommand.TaskDescriptor, diff --git a/pkg/coordinator/wallet/utils.go b/pkg/coordinator/wallet/utils.go new file mode 100644 index 00000000..542a2fbe --- /dev/null +++ b/pkg/coordinator/wallet/utils.go @@ -0,0 +1,81 @@ +package wallet + +import ( + "fmt" + "math/big" + "strings" +) + +func GetReadableBalance(amount *big.Int, unitDigits, maxPreCommaDigitsBeforeTrim, digits int, addPositiveSign, trimAmount bool) string { + // Initialize trimmedAmount and postComma variables to "0" + fullAmount := "" + trimmedAmount := "0" + postComma := "0" + proceed := "" + + if amount != nil { + s := amount.String() + + if amount.Sign() > 0 && addPositiveSign { + proceed = "+" + } else if amount.Sign() < 0 { + proceed = "-" + s = strings.Replace(s, "-", "", 1) + } + + l := len(s) + + // Check if there is a part of the amount before the decimal point + switch { + case l > unitDigits: + // Calculate length of preComma part + l -= unitDigits + // Set preComma to part of the string before the decimal point + trimmedAmount = s[:l] + // Set postComma to part of the string after the decimal point, after removing trailing zeros + postComma = strings.TrimRight(s[l:], "0") + + // Check if the preComma part exceeds the maximum number of digits before the decimal point + if maxPreCommaDigitsBeforeTrim > 0 && l > maxPreCommaDigitsBeforeTrim { + // Reduce the number of digits after the decimal point by the excess number of digits in the preComma part + l -= maxPreCommaDigitsBeforeTrim + if digits < l { + digits = 0 + } else { + digits -= l + } + } + // Check if there is only a part of the amount after the decimal point, and no leading zeros need to be added + case l == unitDigits: + // Set postComma to part of the string after the decimal point, after removing trailing zeros + postComma = strings.TrimRight(s, "0") + // Check if there is only a part of the amount after the decimal point, and leading zeros need to be added + case l != 0: + // Use fmt package to add leading zeros to the string + d := fmt.Sprintf("%%0%dd", unitDigits-l) + // Set postComma to resulting string, after removing trailing zeros + postComma = strings.TrimRight(fmt.Sprintf(d, 0)+s, "0") + } + + fullAmount = trimmedAmount + if postComma != "" { + fullAmount += "." + postComma + } + + // limit floating part + if len(postComma) > digits { + postComma = postComma[:digits] + } + + // set floating point + if postComma != "" { + trimmedAmount += "." + postComma + } + } + + if trimAmount { + return proceed + trimmedAmount + } + + return proceed + fullAmount +} diff --git a/pkg/coordinator/wallet/wallet.go b/pkg/coordinator/wallet/wallet.go index bce4c789..165afff4 100644 --- a/pkg/coordinator/wallet/wallet.go +++ b/pkg/coordinator/wallet/wallet.go @@ -6,7 +6,6 @@ import ( "crypto/ecdsa" "fmt" "math/big" - "strings" "sync" "time" @@ -137,78 +136,8 @@ func (wallet *Wallet) GetNonce() uint64 { } func (wallet *Wallet) GetReadableBalance(unitDigits, maxPreCommaDigitsBeforeTrim, digits int, addPositiveSign, trimAmount bool) string { - // Initialize trimmedAmount and postComma variables to "0" - fullAmount := "" - trimmedAmount := "0" - postComma := "0" - proceed := "" amount := wallet.GetPendingBalance() - - if amount != nil { - s := amount.String() - - if amount.Sign() > 0 && addPositiveSign { - proceed = "+" - } else if amount.Sign() < 0 { - proceed = "-" - s = strings.Replace(s, "-", "", 1) - } - - l := len(s) - - // Check if there is a part of the amount before the decimal point - switch { - case l > unitDigits: - // Calculate length of preComma part - l -= unitDigits - // Set preComma to part of the string before the decimal point - trimmedAmount = s[:l] - // Set postComma to part of the string after the decimal point, after removing trailing zeros - postComma = strings.TrimRight(s[l:], "0") - - // Check if the preComma part exceeds the maximum number of digits before the decimal point - if maxPreCommaDigitsBeforeTrim > 0 && l > maxPreCommaDigitsBeforeTrim { - // Reduce the number of digits after the decimal point by the excess number of digits in the preComma part - l -= maxPreCommaDigitsBeforeTrim - if digits < l { - digits = 0 - } else { - digits -= l - } - } - // Check if there is only a part of the amount after the decimal point, and no leading zeros need to be added - case l == unitDigits: - // Set postComma to part of the string after the decimal point, after removing trailing zeros - postComma = strings.TrimRight(s, "0") - // Check if there is only a part of the amount after the decimal point, and leading zeros need to be added - case l != 0: - // Use fmt package to add leading zeros to the string - d := fmt.Sprintf("%%0%dd", unitDigits-l) - // Set postComma to resulting string, after removing trailing zeros - postComma = strings.TrimRight(fmt.Sprintf(d, 0)+s, "0") - } - - fullAmount = trimmedAmount - if postComma != "" { - fullAmount += "." + postComma - } - - // limit floating part - if len(postComma) > digits { - postComma = postComma[:digits] - } - - // set floating point - if postComma != "" { - trimmedAmount += "." + postComma - } - } - - if trimAmount { - return proceed + trimmedAmount - } - - return proceed + fullAmount + return GetReadableBalance(amount, unitDigits, maxPreCommaDigitsBeforeTrim, digits, addPositiveSign, trimAmount) } func (wallet *Wallet) AwaitReady(ctx context.Context) error {