diff --git a/.github/workflows/bolt_cli_ci.yml b/.github/workflows/bolt_cli_ci.yml index 6ea7c18c7..30e798898 100644 --- a/.github/workflows/bolt_cli_ci.yml +++ b/.github/workflows/bolt_cli_ci.yml @@ -2,6 +2,9 @@ name: Bolt CLI CI on: push: + branches: + - unstable + - main paths: - "bolt-cli/**" pull_request: @@ -42,5 +45,10 @@ jobs: with: crate: cargo-nextest + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Run bolt-cli tests run: cd bolt-cli && cargo nextest run --workspace --retries 3 diff --git a/bolt-cli/.gitignore b/bolt-cli/.gitignore index 12571ac34..fe4b04c0a 100644 --- a/bolt-cli/.gitignore +++ b/bolt-cli/.gitignore @@ -5,3 +5,6 @@ delegations.json pubkeys.json +!test_data/pubkeys.json + +symbiotic-cli/ diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock index 57a5eaa8d..009d4ce57 100644 --- a/bolt-cli/Cargo.lock +++ b/bolt-cli/Cargo.lock @@ -60,9 +60,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alloy" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8ebf106e84a1c37f86244df7da0c7587e697b71a0d565cce079449b85ac6f8" +checksum = "b5b524b8c28a7145d1fe4950f84360b5de3e307601679ff0558ddc20ea229399" dependencies = [ "alloy-consensus", "alloy-contract", @@ -70,6 +70,7 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-network", + "alloy-node-bindings", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", @@ -89,38 +90,39 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4932d790c723181807738cf1ac68198ab581cd699545b155601332541ee47bd" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "num_enum", "strum", ] [[package]] name = "alloy-consensus" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed961a48297c732a5d97ee321aa8bb5009ecadbcb077d8bec90cb54e651629" +checksum = "ae09ffd7c29062431dd86061deefe4e3c6f07fa0d674930095f8dcedb0baf02c" dependencies = [ "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "auto_impl", "c-kzg", "derive_more 1.0.0", + "k256 0.13.4", "serde", ] [[package]] name = "alloy-contract" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460ab80ce4bda1c80bcf96fe7460520476f2c7b734581c6567fac2708e2a60ef" +checksum = "66430a72d5bf5edead101c8c2f0a24bada5ec9f3cf9909b3e08b6d6899b4803e" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-provider", "alloy-pubsub", "alloy-rpc-types-eth", @@ -133,25 +135,25 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cce174ca699ddee3bfb2ec1fbd99ad7efd05eca20c5c888d8320db41f7e8f04" +checksum = "9c8316d83e590f4163b221b8180008f302bda5cf5451202855cdd323e588849c" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-sol-types", ] [[package]] name = "alloy-dyn-abi" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5647fce5a168f9630f935bf7821c4207b1755184edaeba783cb4e11d35058484" +checksum = "ef2364c782a245cf8725ea6dbfca5f530162702b5d685992ea03ce64529136cc" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-type-parser", "alloy-sol-types", "const-hex", @@ -167,18 +169,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ffc577390ce50234e02d841214b3dc0bea6aaaae8e04bbf3cb82e9a45da9eb" +checksum = "5f6cee6a35793f3db8a5ffe60e86c695f321d081a567211245f503e8c498fce8" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "derive_more 1.0.0", "k256 0.13.4", @@ -187,13 +189,13 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69e06cf9c37be824b9d26d6d101114fdde6af0c87de2828b414c05c4b3daa71" +checksum = "5b6aa3961694b30ba53d41006131a2fca3bdab22e4c344e46db2c639e7c2dfdd" dependencies = [ "alloy-eip2930", "alloy-eip7702", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "c-kzg", @@ -205,22 +207,22 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde15e14944a88bd6a57d325e9a49b75558746fe16aaccc79713ae50a6a9574c" +checksum = "e53f7877ded3921d18a0a9556d55bedf84535567198c9edab2aa23106da91855" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-serde", "serde", ] [[package]] name = "alloy-json-abi" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b5671117c38b1c2306891f97ad3828d85487087f54ebe2c7591a055ea5bcea7" +checksum = "b84c506bf264110fa7e90d9924f742f40ef53c6572ea56a0b0bd714a567ed389" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-type-parser", "serde", "serde_json", @@ -228,11 +230,11 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af5979e0d5a7bf9c7eb79749121e8256e59021af611322aee56e77e20776b4b3" +checksum = "3694b7e480728c0b3e228384f223937f14c10caef5a4c766021190fc8f283d35" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-types", "serde", "serde_json", @@ -242,15 +244,15 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "204237129086ce5dc17a58025e93739b01b45313841f98fa339eb1d780511e57" +checksum = "ea94b8ceb5c75d7df0a93ba0acc53b55a22b47b532b600a800a87ef04eb5b0b4" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", @@ -258,22 +260,41 @@ dependencies = [ "async-trait", "auto_impl", "futures-utils-wasm", + "serde", + "serde_json", "thiserror", ] [[package]] name = "alloy-network-primitives" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514f70ee2a953db21631cd817b13a1571474ec77ddc03d47616d5e8203489fde" +checksum = "df9f3e281005943944d15ee8491534a1c7b3cbf7a7de26f8c433b842b93eb5f9" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-serde", "serde", ] +[[package]] +name = "alloy-node-bindings" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9805d126f24be459b958973c0569c73e1aadd27d4535eee82b2b6764aa03616" +dependencies = [ + "alloy-genesis", + "alloy-primitives 0.8.12", + "k256 0.13.4", + "rand", + "serde_json", + "tempfile", + "thiserror", + "tracing", + "url", +] + [[package]] name = "alloy-primitives" version = "0.7.7" @@ -298,9 +319,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71738eb20c42c5fb149571e76536a0f309d142f3957c28791662b96baf77a3d" +checksum = "9fce5dbd6a4f118eecc4719eaa9c7ffc31c315e6c5ccde3642db927802312425" dependencies = [ "alloy-rlp", "bytes", @@ -326,9 +347,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4814d141ede360bb6cd1b4b064f1aab9de391e7c4d0d4d50ac89ea4bc1e25fbd" +checksum = "40c1f9eede27bf4c13c099e8e64d54efd7ce80ef6ea47478aa75d5d74e2dba3b" dependencies = [ "alloy-chains", "alloy-consensus", @@ -336,10 +357,14 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-node-bindings", + "alloy-primitives 0.8.12", "alloy-pubsub", "alloy-rpc-client", + "alloy-rpc-types-anvil", "alloy-rpc-types-eth", + "alloy-signer", + "alloy-signer-local", "alloy-transport", "alloy-transport-http", "alloy-transport-ipc", @@ -366,12 +391,12 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba46eb69ddf7a9925b81f15229cb74658e6eebe5dd30a5b74e2cd040380573" +checksum = "90f1f34232f77341076541c405482e4ae12f0ee7153d8f9969fc1691201b2247" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-transport", "bimap", "futures", @@ -385,9 +410,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26154390b1d205a4a7ac7352aa2eb4f81f391399d4e2f546fb81a2f8bb383f62" +checksum = "da0822426598f95e45dd1ea32a738dac057529a709ee645fcc516ffa4cbde08f" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -407,12 +432,12 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc2bd1e7403463a5f2c61e955bcc9d3072b63aa177442b0f9aa6a6d22a941e3" +checksum = "374dbe0dc3abdc2c964f36b3d3edf9cdb3db29d16bda34aa123f03d810bec1dd" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-pubsub", "alloy-transport", "alloy-transport-http", @@ -433,26 +458,39 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916" +checksum = "c74832aa474b670309c20fffc2a869fa141edab7c79ff7963fad0a08de60bae1" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", + "alloy-rpc-types-anvil", "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", "serde", ] +[[package]] +name = "alloy-rpc-types-anvil" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca97963132f78ddfc60e43a017348e6d52eea983925c23652f5b330e8e02291" +dependencies = [ + "alloy-primitives 0.8.12", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-engine" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886d22d41992287a235af2f3af4299b5ced2bcafb81eb835572ad35747476946" +checksum = "3f56294dce86af23ad6ee8df46cf8b0d292eb5d1ff67dc88a0886051e32b1faf" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "derive_more 1.0.0", @@ -462,14 +500,14 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b034779a4850b4b03f5be5ea674a1cf7d746b2da762b34d1860ab45e48ca27" +checksum = "a8a477281940d82d29315846c7216db45b15e90bcd52309da9f54bcf7ad94a11" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "alloy-sol-types", @@ -481,22 +519,22 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028e72eaa9703e4882344983cfe7636ce06d8cce104a78ea62fd19b46659efc4" +checksum = "4dfa4a7ccf15b2492bb68088692481fd6b2604ccbee1d0d6c44c21427ae4df83" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "serde", "serde_json", ] [[package]] name = "alloy-signer" -version = "0.5.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eca011160d18a7dc6d8cdc1e8dc13e2e86c908f8e41b02aa76e429d6fe7085" +checksum = "2e10aec39d60dc27edcac447302c7803d2371946fb737245320a05b78eb2fafd" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "async-trait", "auto_impl", "elliptic-curve 0.13.8", @@ -506,13 +544,13 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6614f02fc1d5b079b2a4a5320018317b506fd0a6d67c1fd5542a71201724986c" +checksum = "d8396f6dff60700bc1d215ee03d86ff56de268af96e2bf833a14d0bafcab9882" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-signer", "async-trait", "k256 0.13.4", @@ -522,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0900b83f4ee1f45c640ceee596afbc118051921b9438fdb5a3175c1a7e05f8b" +checksum = "9343289b4a7461ed8bab8618504c995c049c082b70c7332efd7b32125633dc05" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -536,9 +574,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41b1e78dde06b5e12e6702fa8c1d30621bf07728ba75b801fb801c9c6a0ba10" +checksum = "4222d70bec485ceccc5d8fd4f2909edd65b5d5e43d4aca0b5dcee65d519ae98f" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -555,9 +593,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91dc311a561a306664393407b88d3e53ae58581624128afd8a15faa5de3627dc" +checksum = "2e17f2677369571b976e51ea1430eb41c3690d344fef567b840bfc0b01b6f83a" dependencies = [ "alloy-json-abi", "const-hex", @@ -572,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d1fbee9e698f3ba176b6e7a145f4aefe6d2b746b611e8bb246fe11a0e9f6c4" +checksum = "aa64d80ae58ffaafdff9d5d84f58d03775f66c84433916dc9a64ed16af5755da" dependencies = [ "serde", "winnow", @@ -582,12 +620,12 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086f41bc6ebcd8cb15f38ba20e47be38dd03692149681ce8061c35d960dbf850" +checksum = "6520d427d4a8eb7aa803d852d7a52ceb0c519e784c292f64bb339e636918cf27" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-macro", "const-hex", "serde", @@ -595,9 +633,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be77579633ebbc1266ae6fd7694f75c408beb1aeb6865d0b18f22893c265a061" +checksum = "f99acddb34000d104961897dbb0240298e8b775a7efffb9fda2a1a3efedd65b3" dependencies = [ "alloy-json-rpc", "base64 0.22.1", @@ -615,9 +653,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fd1a5d0827939847983b46f2f79510361f901dc82f8e3c38ac7397af142c6e" +checksum = "5dc013132e34eeadaa0add7e74164c1503988bfba8bae885b32e0918ba85a8a6" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -630,9 +668,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8073d1186bfeeb8fbdd1292b6f1a0731f3aed8e21e1463905abfae0b96a887a6" +checksum = "063edc0660e81260653cc6a95777c29d54c2543a668aa5da2359fb450d25a1ba" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -649,9 +687,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f27837bb4a1d6c83a28231c94493e814882f0e9058648a97e908a5f3fc9fcf" +checksum = "abd170e600801116d5efe64f74a4fc073dbbb35c807013a7d0a388742aeebba0" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1166,6 +1204,7 @@ name = "bolt" version = "0.3.0-alpha" dependencies = [ "alloy", + "alloy-node-bindings", "blst", "clap", "dotenvy", @@ -4037,9 +4076,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5e0c2ea8db64b2898b62ea2fbd60204ca95e0b2c6bdf53ff768bbe916fbe4d" +checksum = "f76fe0a3e1476bdaa0775b9aec5b869ed9520c2b2fedfe9c6df3618f8ea6290b" dependencies = [ "paste", "proc-macro2", @@ -4699,9 +4738,9 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasmtimer" -version = "0.2.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee" +checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" dependencies = [ "futures", "js-sys", diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml index 1d9addcde..e3ed4bbad 100644 --- a/bolt-cli/Cargo.toml +++ b/bolt-cli/Cargo.toml @@ -23,7 +23,11 @@ blst = "0.3.12" # ethereum ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" } lighthouse_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", rev = "a87f19d" } -alloy = { version = "0.5.2", features = ["full"] } +alloy = { version = "0.6.4", features = [ + "full", + "provider-anvil-api", + "provider-anvil-node", +] } # utils dotenvy = "0.15.7" @@ -37,6 +41,7 @@ rand = "0.8.5" [dev-dependencies] tempfile = "3.13.0" +alloy-node-bindings = "0.6.3" [build-dependencies] tonic-build = "0.12.3" diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 399e613e1..6dfc5ad0d 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -1,12 +1,15 @@ +use std::path::PathBuf; + +use alloy::primitives::{Address, B256, U256}; use clap::{ builder::styling::{AnsiColor, Color, Style}, Parser, Subcommand, ValueEnum, }; use reqwest::Url; -use crate::common::keystore::DEFAULT_KEYSTORE_PASSWORD; +use crate::{common::keystore::DEFAULT_KEYSTORE_PASSWORD, contracts::EigenLayerStrategy}; -/// `bolt` is a CLI tool to interact with Bolt Protocol ✨ +/// `bolt` is a CLI tool to interact with bolt Protocol ✨ #[derive(Parser, Debug, Clone)] #[command(author, version, styles = cli_styles(), about, arg_required_else_help(true))] pub struct Opts { @@ -23,8 +26,14 @@ pub enum Cmd { /// Output a list of pubkeys in JSON format. Pubkeys(PubkeysCommand), - /// Send a preconfirmation request to a Bolt proposer. + /// Send a preconfirmation request to a bolt proposer. Send(Box), + + /// Handle validators in the bolt network. + Validators(ValidatorsCommand), + + /// Handle operators in the bolt network. + Operators(OperatorsCommand), } impl Cmd { @@ -34,6 +43,8 @@ impl Cmd { Cmd::Delegate(cmd) => cmd.run().await, Cmd::Pubkeys(cmd) => cmd.run().await, Cmd::Send(cmd) => cmd.run().await, + Cmd::Validators(cmd) => cmd.run().await, + Cmd::Operators(cmd) => cmd.run().await, } } } @@ -60,7 +71,7 @@ pub struct DelegateCommand { /// The source of the private key. #[clap(subcommand)] - pub source: KeySource, + pub source: SecretsSource, } /// Command for outputting a list of pubkeys in JSON format. @@ -72,13 +83,13 @@ pub struct PubkeysCommand { /// The source of the private keys from which to extract the pubkeys. #[clap(subcommand)] - pub source: KeySource, + pub source: KeysSource, } -/// Command for sending a preconfirmation request to a Bolt proposer. +/// Command for sending a preconfirmation request to a bolt proposer. #[derive(Debug, Clone, Parser)] pub struct SendCommand { - /// Bolt RPC URL to send requests to and fetch lookahead info from. + /// bolt RPC URL to send requests to and fetch lookahead info from. #[clap(long, env = "BOLT_RPC_URL", default_value = "https://rpc-holesky.bolt.chainbound.io")] pub bolt_rpc_url: Url, @@ -86,7 +97,7 @@ pub struct SendCommand { #[clap(long, env = "PRIVATE_KEY", hide_env_values = true)] pub private_key: String, - /// The Bolt Sidecar URL to send requests to. If provided, this will override + /// The bolt Sidecar URL to send requests to. If provided, this will override /// the canonical bolt RPC URL and disregard any registration information. /// /// This is useful for testing and development purposes. @@ -127,6 +138,132 @@ pub struct SendCommand { pub devnet_sidecar_url: Option, } +#[derive(Debug, Clone, Parser)] +pub struct ValidatorsCommand { + #[clap(subcommand)] + pub subcommand: ValidatorsSubcommand, +} + +#[derive(Debug, Clone, Parser)] +pub enum ValidatorsSubcommand { + /// Register a batch of validators. + Register { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + + /// The max gas limit the validator is willing to reserve to commitments. + #[clap(long, env = "MAX_COMMITTED_GAS_LIMIT")] + max_committed_gas_limit: u32, + + /// The authorized operator for the validator. + #[clap(long, env = "AUTHORIZED_OPERATOR")] + authorized_operator: Address, + + /// The path to the JSON pubkeys file, containing an array of BLS public keys. + #[clap(long, env = "PUBKEYS_PATH", default_value = "pubkeys.json")] + pubkeys_path: PathBuf, + + /// The private key to sign the transactions with. + #[clap(long, env = "ADMIN_PRIVATE_KEY")] + admin_private_key: B256, + }, +} + +#[derive(Debug, Clone, Parser)] +pub struct OperatorsCommand { + #[clap(subcommand)] + pub subcommand: OperatorsSubcommand, +} + +#[derive(Debug, Clone, Parser)] +pub enum OperatorsSubcommand { + /// Commands to interact with EigenLayer and bolt. + #[clap(name = "eigenlayer")] // and not eigen-layer + EigenLayer { + #[clap(subcommand)] + subcommand: EigenLayerSubcommand, + }, + /// Commands to interact with Symbiotic and bolt. + Symbiotic { + #[clap(subcommand)] + subcommand: SymbioticSubcommand, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum EigenLayerSubcommand { + /// Step 1: Deposit into a strategy. + Deposit { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: B256, + /// The name of the strategy to deposit into. + #[clap(long, env = "EIGENLAYER_STRATEGY")] + strategy: EigenLayerStrategy, + /// The amount to deposit into the strategy, in ETH + #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT")] + amount: U256, + }, + + /// Step 2: Register into the bolt AVS. + Register { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: B256, + /// The URL of the operator RPC. + #[clap(long, env = "OPERATOR_RPC")] + operator_rpc: Url, + /// The salt for the operator signature. + #[clap(long, env = "OPERATOR_SIGNATURE_SALT")] + salt: B256, + /// The expiry timestamp for the operator signature. + #[clap(long, env = "OPERATOR_SIGNATURE_EXPIRY")] + expiry: U256, + }, + + /// Step 3: Check your operation registration in bolt + Status { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The address of the operator to check. + #[clap(long, env = "OPERATOR_ADDRESS")] + address: Address, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum SymbioticSubcommand { + /// Register into the bolt manager contract as a Symbiotic operator. + Register { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: B256, + /// The URL of the operator RPC. + #[clap(long, env = "OPERATOR_RPC")] + operator_rpc: Url, + }, + /// Check the status of a Symbiotic operator. + Status { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The address of the operator to check. + #[clap(long, env = "OPERATOR_ADDRESS")] + address: Address, + }, +} + /// The action to perform. #[derive(Debug, Clone, ValueEnum)] #[clap(rename_all = "kebab_case")] @@ -138,7 +275,38 @@ pub enum Action { } #[derive(Debug, Clone, Parser)] -pub enum KeySource { +pub enum KeysSource { + /// Use directly local public keys as source. + PublicKeys { + /// The public keys in hex format. Multiple public keys must be seperated by commas. + #[clap(long, env = "PUBLIC_KEYS", value_delimiter = ',', hide_env_values = true)] + public_keys: Vec, + }, + + /// Use local secret keys to generate the associated public keys. + SecretKeys { + /// The private key in hex format. Multiple secret keys must be seperated by commas. + #[clap(long, env = "SECRET_KEYS", value_delimiter = ',', hide_env_values = true)] + secret_keys: Vec, + }, + + /// Use an EIP-2335 filesystem keystore directory as source for public keys. + LocalKeystore { + /// The path to the keystore file. + #[clap(long, env = "KEYSTORE_PATH")] + path: String, + }, + + /// Use a remote DIRK keystore as source for public keys. + Dirk { + /// The options for connecting to the DIRK keystore. + #[clap(flatten)] + opts: DirkOpts, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum SecretsSource { /// Use local secret keys to generate the signed messages. SecretKeys { /// The private key in hex format. @@ -225,7 +393,7 @@ pub struct TlsCredentials { } /// Supported chains for the CLI -#[derive(Debug, Clone, Copy, ValueEnum)] +#[derive(Debug, Clone, Copy, ValueEnum, Hash, PartialEq, Eq)] #[clap(rename_all = "kebab_case")] pub enum Chain { Mainnet, @@ -244,6 +412,16 @@ impl Chain { Chain::Kurtosis => [16, 0, 0, 56], } } + + pub fn from_id(id: u64) -> Option { + match id { + 1 => Some(Self::Mainnet), + 17000 => Some(Self::Holesky), + 3151908 => Some(Self::Kurtosis), + 7014190335 => Some(Self::Helder), + _ => None, + } + } } /// Styles for the CLI application. diff --git a/bolt-cli/src/commands/delegate.rs b/bolt-cli/src/commands/delegate.rs index 24f1e9a15..8dbf096c8 100644 --- a/bolt-cli/src/commands/delegate.rs +++ b/bolt-cli/src/commands/delegate.rs @@ -11,7 +11,7 @@ use serde::Serialize; use tracing::{debug, warn}; use crate::{ - cli::{Action, Chain, DelegateCommand, KeySource}, + cli::{Action, Chain, DelegateCommand, SecretsSource}, common::{ dirk::Dirk, keystore::{keystore_paths, KeystoreError, KeystoreSecret}, @@ -27,7 +27,7 @@ impl DelegateCommand { /// Run the `delegate` command. pub async fn run(self) -> Result<()> { match self.source { - KeySource::SecretKeys { secret_keys } => { + SecretsSource::SecretKeys { secret_keys } => { let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; let signed_messages = generate_from_local_keys( &secret_keys, @@ -45,7 +45,7 @@ impl DelegateCommand { write_to_file(&self.out, &signed_messages)?; println!("Signed delegation messages generated and saved to {}", self.out); } - KeySource::LocalKeystore { opts } => { + SecretsSource::LocalKeystore { opts } => { let keystore_secret = KeystoreSecret::from_keystore_options(&opts)?; let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; let signed_messages = generate_from_keystore( @@ -65,7 +65,7 @@ impl DelegateCommand { write_to_file(&self.out, &signed_messages)?; println!("Signed delegation messages generated and saved to {}", self.out); } - KeySource::Dirk { opts } => { + SecretsSource::Dirk { opts } => { let mut dirk = Dirk::connect(opts.url, opts.tls_credentials).await?; let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; diff --git a/bolt-cli/src/commands/mod.rs b/bolt-cli/src/commands/mod.rs index e2467b1c9..2c4669d8c 100644 --- a/bolt-cli/src/commands/mod.rs +++ b/bolt-cli/src/commands/mod.rs @@ -9,3 +9,9 @@ pub mod pubkeys; /// Module for the bolt `send` command to create and /// broadcast preconfirmations in Bolt. pub mod send; + +/// Module for the validators-related commands to interact with the bolt network. +pub mod validators; + +/// Module for the operators-related commands to interact with the bolt network. +pub mod operators; diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs new file mode 100644 index 000000000..b72ee7dd5 --- /dev/null +++ b/bolt-cli/src/commands/operators.rs @@ -0,0 +1,531 @@ +use alloy::{ + network::EthereumWallet, + node_bindings::WEI_IN_ETHER, + primitives::{utils::format_ether, Bytes}, + providers::{Provider, ProviderBuilder, WalletProvider}, + signers::{local::PrivateKeySigner, SignerSync}, +}; +use eyre::Context; +use tracing::{info, warn}; + +use crate::{ + cli::{ + Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, + }, + common::{bolt_manager::BoltManagerContract, request_confirmation}, + contracts::{ + bolt::{ + BoltEigenLayerMiddleware, + BoltSymbioticMiddleware::{self}, + SignatureWithSaltAndExpiry, + }, + deployments_for_chain, + eigenlayer::{ + AVSDirectory, IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance, + }, + erc20::IERC20::IERC20Instance, + strategy_to_address, + symbiotic::IOptInService, + }, +}; + +impl OperatorsCommand { + pub async fn run(self) -> eyre::Result<()> { + match self.subcommand { + OperatorsSubcommand::EigenLayer { subcommand } => match subcommand { + EigenLayerSubcommand::Deposit { + rpc_url, + strategy, + amount, + operator_private_key, + } => { + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; + let operator = signer.address(); + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer)) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + + let strategy_address = + strategy_to_address(strategy, deployments.eigen_layer.supported_strategies); + let strategy_contract = + IStrategyInstance::new(strategy_address, provider.clone()); + let strategy_manager_address = deployments.eigen_layer.strategy_manager; + let strategy_manager = + IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); + + let token = strategy_contract.underlyingToken().call().await?.token; + + let amount = amount * WEI_IN_ETHER; + + info!(%strategy, %token, amount = format_ether(amount), ?operator, "Depositing funds into EigenLayer strategy"); + + request_confirmation(); + + let token_erc20 = IERC20Instance::new(token, provider.clone()); + + let balance = token_erc20 + .balanceOf(provider.clone().default_signer_address()) + .call() + .await? + ._0; + + info!("Operator token balance: {}", format_ether(balance)); + + let result = + token_erc20.approve(strategy_manager_address, amount).send().await?; + + info!(hash = ?result.tx_hash(), "Approving transfer of {} {:?}, awaiting receipt...", amount, strategy); + let result = result.watch().await?; + info!("Approval transaction included. Transaction hash: {:?}", result); + + let result = strategy_manager + .depositIntoStrategy(strategy_address, token, amount) + .send() + .await?; + + info!(hash = ?result.tx_hash(), "Submitted deposit transaction, awaiting receipt..."); + let receipt = result.get_receipt().await?; + + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deposited collateral into strategy"); + + Ok(()) + } + EigenLayerSubcommand::Register { + rpc_url, + operator_rpc, + salt, + expiry, + operator_private_key, + } => { + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer.clone())) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering EigenLayer operator"); + + request_confirmation(); + + let deployments = deployments_for_chain(chain); + + let bolt_avs_address = deployments.bolt.eigenlayer_middleware; + let bolt_eigenlayer_middleware = + BoltEigenLayerMiddleware::new(bolt_avs_address, provider.clone()); + + let avs_directory = + AVSDirectory::new(deployments.eigen_layer.avs_directory, provider.clone()); + let signature_digest_hash = avs_directory + .calculateOperatorAVSRegistrationDigestHash( + provider.clone().default_signer_address(), + bolt_avs_address, + salt, + expiry, + ) + .call() + .await? + ._0; + + let signature = + Bytes::from(signer.sign_hash_sync(&signature_digest_hash)?.as_bytes()); + let signature = SignatureWithSaltAndExpiry { signature, expiry, salt }; + + let result = bolt_eigenlayer_middleware + .registerOperator(operator_rpc.to_string(), signature) + .send() + .await?; + + info!( + hash = ?result.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = result.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); + + Ok(()) + } + EigenLayerSubcommand::Status { rpc_url: rpc, address } => { + let provider = ProviderBuilder::new().on_http(rpc.clone()); + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + let bolt_manager = + BoltManagerContract::new(deployments.bolt.manager, provider.clone()); + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "EigenLayer operator is registered"); + } else { + warn!(?address, "Operator not registered"); + } + + Ok(()) + } + }, + OperatorsSubcommand::Symbiotic { subcommand } => match subcommand { + SymbioticSubcommand::Register { operator_rpc, operator_private_key, rpc_url } => { + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer.clone())) + .on_http(rpc_url); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); + + request_confirmation(); + + // Check if operator is opted in to the bolt network + if !IOptInService::new( + deployments.symbiotic.network_opt_in_service, + provider.clone(), + ) + .isOptedIn(signer.address(), deployments.symbiotic.network) + .call() + .await? + ._0 + { + eyre::bail!( + "Operator with address {} not opted in to the bolt network ({})", + signer.address(), + deployments.symbiotic.network + ); + } + + let middleware = BoltSymbioticMiddleware::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); + + let pending = + middleware.registerOperator(operator_rpc.to_string()).send().await?; + + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); + + Ok(()) + } + SymbioticSubcommand::Status { rpc_url, address } => { + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + let bolt_manager = + BoltManagerContract::new(deployments.bolt.manager, provider.clone()); + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "Symbiotic operator is registered"); + } else { + warn!(?address, "Operator not registered"); + } + + Ok(()) + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use std::process::{Command, Output}; + + use crate::{ + cli::{ + Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, + }, + contracts::{ + deployments_for_chain, + eigenlayer::{DelegationManager, IStrategy, OperatorDetails}, + strategy_to_address, EigenLayerStrategy, + }, + }; + use alloy::{ + network::EthereumWallet, + primitives::{address, keccak256, utils::parse_units, Address, B256, U256}, + providers::{ext::AnvilApi, Provider, ProviderBuilder, WalletProvider}, + signers::local::PrivateKeySigner, + sol_types::SolValue, + }; + use rand::Rng; + + #[tokio::test] + async fn test_eigenlayer_flow() { + let mut rnd = rand::thread_rng(); + let secret_key = B256::from(rnd.gen::<[u8; 32]>()); + let wallet = PrivateKeySigner::from_bytes(&secret_key).expect("valid private key"); + + let rpc_url = "https://holesky.drpc.org"; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(wallet)) + .on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let account = provider.default_signer_address(); + + // Add balance to the operator + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let deployments = deployments_for_chain(Chain::Holesky); + + let weth_strategy_address = strategy_to_address( + EigenLayerStrategy::WEth, + deployments.eigen_layer.supported_strategies, + ); + let strategy = IStrategy::new(weth_strategy_address, provider.clone()); + let weth_address = strategy.underlyingToken().call().await.expect("underlying token").token; + + // Mock WETH balance using the Anvil API. + let hashed_slot = keccak256((account, U256::from(3)).abi_encode()); + let mocked_balance: U256 = parse_units("100.0", "ether").expect("parse ether").into(); + provider + .anvil_set_storage_at(weth_address, hashed_slot.into(), mocked_balance.into()) + .await + .expect("to set storage"); + + let random_address = Address::from(rnd.gen::<[u8; 20]>()); + + // 1. Register the operator into EigenLayer. This should be done by the operator using the + // EigenLayer CLI, but we do it here for testing purposes. + + let delegation_manager = + DelegationManager::new(deployments.eigen_layer.delegation_manager, provider.clone()); + let receipt = delegation_manager + .registerAsOperator( + OperatorDetails { + earningsReceiver: random_address, + delegationApprover: Address::ZERO, + stakerOptOutWindowBlocks: 32, + }, + "https://bolt.chainbound.io/rpc".to_string(), + ) + .send() + .await + .expect("to send register as operator") + .get_receipt() + .await + .expect("to get receipt for register as operator"); + + assert!(receipt.status(), "operator should be registered"); + println!("Registered operator with address {}", account); + + let is_operator = delegation_manager + .isOperator(account) + .call() + .await + .expect("to check if operator is registered") + ._0; + println!("is operator {}", is_operator); + + // 2. Deposit into the strategy + + let deposit_into_strategy = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::Deposit { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: secret_key, + strategy: EigenLayerStrategy::WEth, + amount: U256::from(1), + }, + }, + }; + + deposit_into_strategy.run().await.expect("to deposit into strategy"); + + // 3. Register the operator into Bolt AVS + + let register_operator = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::Register { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: secret_key, + operator_rpc: "https://bolt.chainbound.io/rpc".parse().expect("valid url"), + salt: B256::ZERO, + expiry: U256::MAX, + }, + }, + }; + + register_operator.run().await.expect("to register operator"); + + // 4. Check operator registration + let check_operator_registration = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::Status { + rpc_url: anvil_url.parse().expect("valid url"), + address: account, + }, + }, + }; + + check_operator_registration.run().await.expect("to check operator registration"); + } + + /// Ignored since it requires Symbiotic CLI: https://docs.symbiotic.fi/guides/cli/#installation + /// To run this test, install the CLI, and then move the binary in the `symbiotic-cli` directory + /// which is git-ignored for this purpose. + #[tokio::test] + #[ignore = "requires Symbiotic CLI installed"] + async fn test_symbiotic_flow() { + let mut rnd = rand::thread_rng(); + let secret_key = B256::from(rnd.gen::<[u8; 32]>()); + let wallet = PrivateKeySigner::from_bytes(&secret_key).expect("valid private key"); + + let rpc_url = "https://rpc-holesky.bolt.chainbound.io/rpc"; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(wallet)) + .on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let account = provider.default_signer_address(); + + // Add balance to the operator + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let deployments = deployments_for_chain(Chain::Holesky); + + let weth_address = address!("94373a4919B3240D86eA41593D5eBa789FEF3848"); + + // Mock WETH balance using the Anvil API. + let hashed_slot = keccak256((account, U256::from(3)).abi_encode()); + let mocked_balance: U256 = parse_units("100.0", "ether").expect("parse ether").into(); + provider + .anvil_set_storage_at(weth_address, hashed_slot.into(), mocked_balance.into()) + .await + .expect("to set storage"); + + let print_output = |output: Output| { + println!("{}", String::from_utf8_lossy(&output.stdout)); + }; + + // We now follow the steps described in the Holesky guide + + let register_operator = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("register-operator") + .arg("--private-key") + .arg(secret_key.to_string()) + .output() + .expect("to register operator"); + + print_output(register_operator); + + let opt_in_network = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("opt-in-network") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(deployments.symbiotic.network.to_string()) + .output() + .expect("to opt-in-network"); + + print_output(opt_in_network); + + let vault = deployments.symbiotic.supported_vaults[3]; // WETH vault + + let opt_in_vault = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("opt-in-vault") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(vault.to_string()) + .output() + .expect("to opt-in-vault"); + + print_output(opt_in_vault); + + let deposit = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("deposit") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(vault.to_string()) + .arg("1") // 1 ether + .output() + .expect("to opt-in-vault"); + + print_output(deposit); + + let register_into_bolt = OperatorsCommand { + subcommand: OperatorsSubcommand::Symbiotic { + subcommand: SymbioticSubcommand::Register { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: secret_key, + operator_rpc: "https://bolt.chainbound.io".parse().expect("valid url"), + }, + }, + }; + + register_into_bolt.run().await.expect("to register into bolt"); + + let check_status = OperatorsCommand { + subcommand: OperatorsSubcommand::Symbiotic { + subcommand: SymbioticSubcommand::Status { + rpc_url: anvil_url.parse().expect("valid url"), + address: account, + }, + }, + }; + + check_status.run().await.expect("to check operator status"); + } +} diff --git a/bolt-cli/src/commands/pubkeys.rs b/bolt-cli/src/commands/pubkeys.rs index ba10ff38a..0609fbe78 100644 --- a/bolt-cli/src/commands/pubkeys.rs +++ b/bolt-cli/src/commands/pubkeys.rs @@ -3,10 +3,10 @@ use eyre::Result; use lighthouse_eth2_keystore::Keystore; use crate::{ - cli::{KeySource, PubkeysCommand}, + cli::{KeysSource, PubkeysCommand}, common::{ dirk::Dirk, - keystore::{keystore_paths, KeystoreError, KeystoreSecret}, + keystore::{keystore_paths, KeystoreError}, write_to_file, }, pb::eth2_signer_api::Account, @@ -15,20 +15,23 @@ use crate::{ impl PubkeysCommand { pub async fn run(self) -> Result<()> { match self.source { - KeySource::SecretKeys { secret_keys } => { + KeysSource::PublicKeys { public_keys } => { + write_to_file(&self.out, &public_keys)?; + println!("Pubkeys saved to {}", self.out); + } + KeysSource::SecretKeys { secret_keys } => { let pubkeys = list_from_local_keys(&secret_keys)?; write_to_file(&self.out, &pubkeys)?; println!("Pubkeys generated and saved to {}", self.out); } - KeySource::LocalKeystore { opts } => { - let keystore_secret = KeystoreSecret::from_keystore_options(&opts)?; - let pubkeys = list_from_keystore(&opts.path, keystore_secret)?; + KeysSource::LocalKeystore { path } => { + let pubkeys = list_from_keystore(&path)?; write_to_file(&self.out, &pubkeys)?; - println!("Pubkeys generated and saved to {}", self.out); + println!("Pubkeys generated from local keystore and saved to {}", self.out); } - KeySource::Dirk { opts } => { + KeysSource::Dirk { opts } => { // Note: we don't need to unlock wallets to list pubkeys let mut dirk = Dirk::connect(opts.url, opts.tls_credentials).await?; @@ -36,7 +39,7 @@ impl PubkeysCommand { let pubkeys = list_from_dirk_accounts(&accounts)?; write_to_file(&self.out, &pubkeys)?; - println!("Pubkeys generated and saved to {}", self.out); + println!("Pubkeys generated from Dirk and saved to {}", self.out); } } @@ -57,18 +60,19 @@ pub fn list_from_local_keys(secret_keys: &[String]) -> Result> } /// Derive public keys from the keystore files in the provided directory. -pub fn list_from_keystore( - keys_path: &str, - keystore_secret: KeystoreSecret, -) -> Result> { +pub fn list_from_keystore(keys_path: &str) -> Result> { let keystores_paths = keystore_paths(keys_path)?; let mut pubkeys = Vec::with_capacity(keystores_paths.len()); for path in keystores_paths { let ks = Keystore::from_json_file(path).map_err(KeystoreError::Eth2Keystore)?; - let password = keystore_secret.get(ks.pubkey()).ok_or(KeystoreError::MissingPassword)?; - let kp = ks.decrypt_keypair(password.as_bytes()).map_err(KeystoreError::Eth2Keystore)?; - let pubkey = BlsPublicKey::try_from(kp.pk.serialize().to_vec().as_ref())?; + let pubkey = BlsPublicKey::try_from( + ks.public_key() + .expect("to parse public key from keystore") + .serialize() + .to_vec() + .as_ref(), + )?; pubkeys.push(pubkey); } diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs new file mode 100644 index 000000000..4580e586d --- /dev/null +++ b/bolt-cli/src/commands/validators.rs @@ -0,0 +1,117 @@ +use alloy::{ + network::EthereumWallet, + providers::{Provider, ProviderBuilder}, + signers::local::PrivateKeySigner, +}; +use ethereum_consensus::crypto::PublicKey as BlsPublicKey; +use eyre::Context; +use tracing::info; + +use crate::{ + cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, + common::{hash::compress_bls_pubkey, request_confirmation}, + contracts::{bolt::BoltValidators, deployments_for_chain}, +}; + +impl ValidatorsCommand { + pub async fn run(self) -> eyre::Result<()> { + match self.subcommand { + ValidatorsSubcommand::Register { + max_committed_gas_limit, + pubkeys_path, + admin_private_key, + authorized_operator, + rpc_url, + } => { + let signer = PrivateKeySigner::from_bytes(&admin_private_key) + .wrap_err("valid private key")?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer)) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let bolt_validators_address = deployments_for_chain(chain).bolt.validators; + + let pubkeys_file = std::fs::File::open(&pubkeys_path)?; + let keys: Vec = serde_json::from_reader(pubkeys_file)?; + let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); + + info!( + validators = ?keys.len(), + ?max_committed_gas_limit, + ?authorized_operator, + ?chain, + "Registering validators into bolt", + ); + + let bolt_validators = + BoltValidators::new(bolt_validators_address, provider.clone()); + + request_confirmation(); + + let pending = bolt_validators + .batchRegisterValidatorsUnsafe( + pubkey_hashes, + max_committed_gas_limit, + authorized_operator, + ) + .send() + .await?; + + info!( + hash = ?pending.tx_hash(), + "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..." + ); + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Successfully registered validators into bolt"); + + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use alloy::{ + primitives::{Address, B256, U256}, + providers::{ext::AnvilApi, Provider, ProviderBuilder}, + signers::k256::ecdsa::SigningKey, + }; + + use crate::cli::{ValidatorsCommand, ValidatorsSubcommand}; + + #[tokio::test] + async fn test_register_validators() { + let rpc_url = "https://holesky.drpc.org"; + let provider = ProviderBuilder::new().on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let mut rnd = rand::thread_rng(); + let secret_key = SigningKey::random(&mut rnd); + let account = Address::from_private_key(&secret_key); + + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let command = ValidatorsCommand { + subcommand: ValidatorsSubcommand::Register { + max_committed_gas_limit: 30_000_000, + admin_private_key: B256::try_from(secret_key.to_bytes().as_slice()).unwrap(), + authorized_operator: account, + pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), + rpc_url: anvil_url.parse().unwrap(), + }, + }; + + command.run().await.expect("run command"); + } +} diff --git a/bolt-cli/src/common/bolt_manager.rs b/bolt-cli/src/common/bolt_manager.rs index 261347ab3..35a763a68 100644 --- a/bolt-cli/src/common/bolt_manager.rs +++ b/bolt-cli/src/common/bolt_manager.rs @@ -90,6 +90,8 @@ sol! { function getProposerStatus(bytes32 pubkeyHash) external view returns (ProposerStatus memory); + function isOperator(address operator) public view returns (bool); + error InvalidQuery(); error ValidatorDoesNotExist(); } diff --git a/bolt-cli/src/common/hash.rs b/bolt-cli/src/common/hash.rs new file mode 100644 index 000000000..0816134c2 --- /dev/null +++ b/bolt-cli/src/common/hash.rs @@ -0,0 +1,24 @@ +use alloy::primitives::{keccak256, FixedBytes, B512}; +use ethereum_consensus::crypto::PublicKey as BlsPublicKey; + +/// A 20-byte compressed hash of a BLS public key. +/// +/// Reference: https://github.com/chainbound/bolt/blob/bec46baae6d7c16dddd81e5e72710ca8e3064f82/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol#L65-L69 +pub type CompressedHash = FixedBytes<20>; + +/// Compress a BLS public key into a 20-byte hash. +pub fn compress_bls_pubkey(pubkey: &BlsPublicKey) -> CompressedHash { + let mut onchain_pubkey_repr = B512::ZERO; + + // copy the pubkey bytes into the rightmost 48 bytes of the 512-bit buffer. + // the result should look like this: + // + // 0x00000000000000000000000000000000b427fd179b35ef085409e4a98fb3ab84ee29c689df5c64020eab0b20a4f91170f610177db172dc091682df627c9f4021 + // |<---------- 16 bytes ---------->||<----------------------------------------- 48 bytes ----------------------------------------->| + onchain_pubkey_repr[16..].copy_from_slice(pubkey.as_ref()); + + // hash the pubkey + let hash = keccak256(onchain_pubkey_repr); + + CompressedHash::from_slice(hash.get(0..20).expect("hash is longer than 20 bytes")) +} diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index d730df81b..6f51fb86c 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -1,8 +1,9 @@ -use std::{fs, path::PathBuf}; +use std::{fs, io::Write, path::PathBuf}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::{Context, Result}; use serde::Serialize; +use tracing::info; /// BoltManager contract bindings. pub mod bolt_manager; @@ -16,6 +17,9 @@ pub mod keystore; /// Utilities for signing and verifying messages. pub mod signing; +/// Utilities for hashing messages and custom types. +pub mod hash; + /// Parse a BLS public key from a string pub fn parse_bls_public_key(delegatee_pubkey: &str) -> Result { let hex_pk = delegatee_pubkey.strip_prefix("0x").unwrap_or(delegatee_pubkey); @@ -32,3 +36,34 @@ pub fn write_to_file(out: &str, data: &T) -> Result<()> { serde_json::to_writer_pretty(out_file, data)?; Ok(()) } + +/// Asks whether the user wants to proceed further. If not, the process is exited. +pub fn request_confirmation() { + #[cfg(test)] + return; + + loop { + info!("Do you want to continue? (yes/no): "); + + print!("Answer: "); + std::io::stdout().flush().expect("Failed to flush"); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input).expect("Failed to read input"); + + let input = input.trim().to_lowercase(); + + match input.as_str() { + "yes" | "y" => { + return; + } + "no" | "n" => { + info!("Aborting"); + std::process::exit(0); + } + _ => { + println!("Invalid input. Please type 'yes' or 'no'."); + } + } + } +} diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs new file mode 100644 index 000000000..a07e226df --- /dev/null +++ b/bolt-cli/src/contracts/bolt.rs @@ -0,0 +1,55 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltValidators { + /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them + /// @dev This function allows anyone to register a list of Validators. + /// @param pubkeyHashes List of BLS public key hashes for the Validators to be registered + /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations + /// @param authorizedOperator The address of the authorized operator + function batchRegisterValidatorsUnsafe(bytes20[] calldata pubkeyHashes, uint32 maxCommittedGasLimit, address authorizedOperator); + + error KeyNotFound(); + error InvalidQuery(); + #[derive(Debug)] + error ValidatorDoesNotExist(bytes20 pubkeyHash); + error InvalidAuthorizedOperator(); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + struct SignatureWithSaltAndExpiry { + bytes signature; + bytes32 salt; + uint256 expiry; + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltEigenLayerMiddleware { + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. + /// The msg.sender of this call will be the operator address. + function registerOperator(string calldata rpc, SignatureWithSaltAndExpiry calldata operatorSignature) public; + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltSymbioticMiddleware { + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// msg.sender must be an operator in the Symbiotic network. + function registerOperator(string calldata rpc) public; + + /// @notice Get the collaterals and amounts staked by an operator across the supported strategies. + /// + /// @param operator The operator address to get the collaterals and amounts staked for. + /// @return collaterals The collaterals staked by the operator. + /// @dev Assumes that the operator is registered and enabled. + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + } +} diff --git a/bolt-cli/src/contracts/eigenlayer.rs b/bolt-cli/src/contracts/eigenlayer.rs new file mode 100644 index 000000000..28625f1c4 --- /dev/null +++ b/bolt-cli/src/contracts/eigenlayer.rs @@ -0,0 +1,214 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + // Reference source code: https://github.com/Layr-Labs/eigenlayer-contracts/blob/testnet-holesky/src/contracts/interfaces/IStrategy.sol + // + // NOTE: IERC20 tokens are replaced with `address` because there's no support for it: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html#solidity + + /** + * @title Minimal interface for an `Strategy` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Custom `Strategy` implementations may expand extensively on this interface. + */ + interface IStrategy { + /** + * @notice Used to deposit tokens into this Strategy + * @param token is the ERC20 token being deposited + * @param amount is the amount of token being deposited + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. + * @return newShares is the number of new shares issued at the current exchange ratio. + */ + function deposit(address token, uint256 amount) external returns (uint256 shares); + + /** + * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address + * @param recipient is the address to receive the withdrawn funds + * @param token is the ERC20 token being transferred out + * @param amountShares is the amount of shares being withdrawn + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * other functions, and individual share balances are recorded in the strategyManager as well. + */ + function withdraw(address recipient, address token, uint256 amountShares) external; + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlying(uint256 amountShares) external returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToShares(uint256 amountUnderlying) external returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications + */ + function userUnderlying(address user) external returns (uint256); + + /** + * @notice convenience function for fetching the current total shares of `user` in this strategy, by + * querying the `strategyManager` contract + */ + function shares(address user) external view returns (uint256); + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlyingView(uint256 amountShares) external view returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToShares`, this function guarantees no state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToSharesView(uint256 amountUnderlying) external view returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications + */ + function userUnderlyingView(address user) external view returns (uint256); + + /// @notice The underlying token for shares in this Strategy + function underlyingToken() external view returns (address token); + + /// @notice The total number of extant shares in this Strategy + function totalShares() external view returns (uint256); + + /// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail. + function explanation() external view returns (string memory); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + /** + * @title Interface for the primary entrypoint for funds into EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice See the `StrategyManager` contract itself for implementation details. + */ + interface IStrategyManager { + /** + * @notice Emitted when a new deposit occurs on behalf of `staker`. + * @param staker Is the staker who is depositing funds into EigenLayer. + * @param strategy Is the strategy that `staker` has deposited into. + * @param token Is the token that `staker` deposited. + * @param shares Is the number of new shares `staker` has been granted in `strategy`. + */ + event Deposit(address staker, address token, address strategy, uint256 shares); + + /** + * @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender` + * @param strategy is the specified strategy where deposit is to be made, + * @param token is the denomination in which the deposit is to be made, + * @param amount is the amount of token to be deposited in the strategy by the staker + * @return shares The amount of new shares in the `strategy` created as part of the action. + * @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf. + * @dev Cannot be called by an address that is 'frozen' (this function will revert if the `msg.sender` is frozen). + * + * WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors + * where the token balance and corresponding strategy shares are not in sync upon reentrancy. + */ + function depositIntoStrategy(address strategy, address token, uint256 amount) external returns (uint256 shares); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + struct SignatureWithSaltAndExpiry { + bytes signature; + bytes32 salt; + uint256 expiry; + } + + #[allow(missing_docs)] + #[sol(rpc)] + /** + * @title Interface for the primary entrypoint for funds into EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice See the `StrategyManager` contract itself for implementation details. + */ + interface AVSDirectory { + + /** + * @notice Called by an avs to register an operator with the avs. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS(address operator, SignatureWithSaltAndExpiry memory operatorSignature) external; + + /** + * @notice Calculates the digest hash to be signed by an operator to register with an AVS + * @param operator The account registering as an operator + * @param avs The AVS the operator is registering to + * @param salt A unique and single use value associated with the approver signature. + * @param expiry Time after which the approver's signature becomes invalid + */ + function calculateOperatorAVSRegistrationDigestHash( + address operator, + address avs, + bytes32 salt, + uint256 expiry + ) external view returns (bytes32); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + // @notice Struct used for storing information about a single operator who has registered with EigenLayer + struct OperatorDetails { + // @notice address to receive the rewards that the operator earns via serving applications built on EigenLayer. + address earningsReceiver; + /** + * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". + * @dev Signature verification follows these rules: + * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. + * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. + * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". + */ + address delegationApprover; + /** + * @notice A minimum delay -- measured in blocks -- enforced between: + * 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing` + * and + * 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate` + * @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails, + * then they are only allowed to either increase this value or keep it the same. + */ + uint32 stakerOptOutWindowBlocks; + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface DelegationManager { + function registerAsOperator(OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI) external; + + function isOperator(address operator) public view returns (bool); + } +} diff --git a/bolt-cli/src/contracts/erc20.rs b/bolt-cli/src/contracts/erc20.rs new file mode 100644 index 000000000..469ce7e0a --- /dev/null +++ b/bolt-cli/src/contracts/erc20.rs @@ -0,0 +1,83 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + // Reference source code: https://github.com/openzeppelin/openzeppelin-contracts/blob/HEAD/contracts/token/ERC20/IERC20.sol + + /** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ + interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); + } +} diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs new file mode 100644 index 000000000..52603efff --- /dev/null +++ b/bolt-cli/src/contracts/mod.rs @@ -0,0 +1,148 @@ +use std::collections::HashMap; + +use alloy::primitives::{address, Address}; +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +use crate::cli::Chain; + +pub mod bolt; +pub mod eigenlayer; +pub mod erc20; +pub mod symbiotic; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Contracts { + pub bolt: Bolt, + pub symbiotic: Symbiotic, + pub eigen_layer: EigenLayer, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Bolt { + pub validators: Address, + pub parameters: Address, + pub manager: Address, + pub eigenlayer_middleware: Address, + pub symbiotic_middleware: Address, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Symbiotic { + pub network: Address, + pub operator_registry: Address, + pub network_opt_in_service: Address, + pub vault_factory: Address, + pub vault_configurator: Address, + pub network_registry: Address, + pub network_middleware_service: Address, + pub middleware: Address, + pub supported_vaults: [Address; 6], +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EigenLayer { + pub avs_directory: Address, + pub delegation_manager: Address, + pub strategy_manager: Address, + pub middleware: Address, + pub supported_strategies: EigenLayerStrategies, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EigenLayerStrategies { + st_eth: Address, + r_eth: Address, + w_eth: Address, + cb_eth: Address, + m_eth: Address, +} + +#[derive(Copy, Clone, Serialize, Deserialize, Debug, ValueEnum)] +#[allow(clippy::enum_variant_names)] +#[serde(rename_all = "kebab-case")] +pub enum EigenLayerStrategy { + StEth, + REth, + WEth, + CbEth, + MEth, +} + +impl std::fmt::Display for EigenLayerStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + EigenLayerStrategy::StEth => "stETH", + EigenLayerStrategy::REth => "rETH", + EigenLayerStrategy::WEth => "wETH", + EigenLayerStrategy::CbEth => "cbETH", + EigenLayerStrategy::MEth => "mETH", + }; + write!(f, "{}", output) + } +} + +pub fn strategy_to_address( + strategy: EigenLayerStrategy, + addresses: EigenLayerStrategies, +) -> Address { + match strategy { + EigenLayerStrategy::StEth => addresses.st_eth, + EigenLayerStrategy::REth => addresses.r_eth, + EigenLayerStrategy::WEth => addresses.w_eth, + EigenLayerStrategy::CbEth => addresses.cb_eth, + EigenLayerStrategy::MEth => addresses.m_eth, + } +} + +pub fn deployments() -> HashMap { + let mut deployments = HashMap::new(); + deployments.insert(Chain::Holesky, HOLESKY_DEPLOYMENTS); + + deployments +} + +pub fn deployments_for_chain(chain: Chain) -> Contracts { + deployments().get(&chain).cloned().expect("no deployments for chain") +} + +const HOLESKY_DEPLOYMENTS: Contracts = Contracts { + bolt: Bolt { + validators: address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8"), + parameters: address!("20d1cf3A5BD5928dB3118b2CfEF54FDF9fda5c12"), + manager: address!("440202829b493F9FF43E730EB5e8379EEa3678CF"), + eigenlayer_middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), + symbiotic_middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), + }, + symbiotic: Symbiotic { + network: address!("b017002D8024d8c8870A5CECeFCc63887650D2a4"), + operator_registry: address!("6F75a4ffF97326A00e52662d82EA4FdE86a2C548"), + network_opt_in_service: address!("58973d16FFA900D11fC22e5e2B6840d9f7e13401"), + vault_factory: address!("407A039D94948484D356eFB765b3c74382A050B4"), + vault_configurator: address!("D2191FE92987171691d552C219b8caEf186eb9cA"), + network_registry: address!("7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9"), + network_middleware_service: address!("62a1ddfD86b4c1636759d9286D3A0EC722D086e3"), + middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), + supported_vaults: [ + address!("c79c533a77691641d52ebD5e87E51dCbCaeb0D78"), + address!("e5708788c90e971f73D928b7c5A8FD09137010e0"), + address!("11c5b9A9cd8269580aDDbeE38857eE451c1CFacd"), + address!("C56Ba584929c6f381744fA2d7a028fA927817f2b"), + address!("cDdeFfcD2bA579B8801af1d603812fF64c301462"), + address!("91e84e12Bb65576C0a6614c5E6EbbB2eA595E10f"), + ], + }, + eigen_layer: EigenLayer { + avs_directory: address!("055733000064333CaDDbC92763c58BF0192fFeBf"), + delegation_manager: address!("A44151489861Fe9e3055d95adC98FbD462B948e7"), + strategy_manager: address!("dfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6"), + middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), + supported_strategies: EigenLayerStrategies { + st_eth: address!("7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3"), + r_eth: address!("3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0"), + w_eth: address!("80528D6e9A2BAbFc766965E0E26d5aB08D9CFaF9"), + cb_eth: address!("70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6"), + m_eth: address!("accc5A86732BE85b5012e8614AF237801636F8e5"), + }, + }, +}; diff --git a/bolt-cli/src/contracts/symbiotic.rs b/bolt-cli/src/contracts/symbiotic.rs new file mode 100644 index 000000000..0ebe13e01 --- /dev/null +++ b/bolt-cli/src/contracts/symbiotic.rs @@ -0,0 +1,31 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + // Reference source code: https://github.com/symbioticfi/core/blob/main/src/interfaces/service/IOptInService.sol + interface IOptInService { + /** + * @notice Get if a given "who" is opted-in to a particular "where" entity at a given timestamp using a hint. + * @param who address of the "who" + * @param where address of the "where" entity + * @param timestamp time point to get if the "who" is opted-in at + * @param hint hint for the checkpoint index + * @return if the "who" is opted-in at the given timestamp + */ + function isOptedInAt( + address who, + address where, + uint48 timestamp, + bytes calldata hint + ) external view returns (bool); + + /** + * @notice Check if a given "who" is opted-in to a particular "where" entity. + * @param who address of the "who" + * @param where address of the "where" entity + * @return if the "who" is opted-in + */ + function isOptedIn(address who, address where) external view returns (bool); + } +} diff --git a/bolt-cli/src/main.rs b/bolt-cli/src/main.rs index dc59ec8ad..cdf5a003b 100644 --- a/bolt-cli/src/main.rs +++ b/bolt-cli/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use tracing::error; /// CLI command definitions and options. mod cli; @@ -12,13 +13,16 @@ mod common; /// Protocol Buffers definitions generated by `prost`. mod pb; +/// Contracts and interfaces bindings for interacting with the Bolt network. +mod contracts; + #[tokio::main] async fn main() -> eyre::Result<()> { let _ = dotenvy::dotenv(); - let _ = tracing_subscriber::fmt::try_init(); + let _ = tracing_subscriber::fmt().with_target(false).try_init(); if let Err(err) = rustls::crypto::ring::default_provider().install_default() { - eprintln!("Failed to install default TLS provider: {:?}", err); + error!("Failed to install default TLS provider: {:?}", err); } cli::Opts::parse().command.run().await diff --git a/bolt-cli/test_data/pubkeys.json b/bolt-cli/test_data/pubkeys.json new file mode 100644 index 000000000..091cb69e8 --- /dev/null +++ b/bolt-cli/test_data/pubkeys.json @@ -0,0 +1,3 @@ +[ + "0x0f6db8a87c31897f504bbd22e7a83a3613607499ae94c0fce4a6e0cab13692a5f69114cff399d96a38f78a77b045e9c7" +] diff --git a/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol b/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol deleted file mode 100644 index 3fa63dbe8..000000000 --- a/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {Script, console} from "forge-std/Script.sol"; - -import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; -import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; -import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; -import {IStrategy, IERC20} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; -import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; - -import {BoltEigenLayerMiddlewareV2} from "../../../src/contracts/BoltEigenLayerMiddlewareV2.sol"; -import {IBoltMiddlewareV1} from "../../../src/interfaces/IBoltMiddlewareV1.sol"; -import {IBoltManagerV2} from "../../../src/interfaces/IBoltManagerV2.sol"; - -contract RegisterEigenLayerOperator is Script { - struct OperatorConfig { - string rpc; - bytes32 salt; - uint256 expiry; - } - - function S01_depositIntoStrategy() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - - IStrategyManager strategyManager = _readStrategyManager(); - - string memory json = vm.readFile("config/holesky/operators/eigenlayer/depositIntoStrategy.json"); - - IStrategy strategy = IStrategy(vm.parseJsonAddress(json, ".strategy")); - IERC20 token = IERC20(vm.parseJsonAddress(json, ".token")); - uint256 amount = vm.parseJsonUint(json, ".amount") * 1 ether; - - vm.startBroadcast(operatorSk); - // Allowance must be set before depositing - token.approve(address(strategyManager), amount); - strategyManager.depositIntoStrategy(strategy, token, amount); - console.log("Successfully run StrategyManager.depositIntoStrategy"); - vm.stopBroadcast(); - } - - function S02_registerIntoBoltAVS() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - address operator = vm.addr(operatorSk); - - BoltEigenLayerMiddlewareV2 middleware = _readMiddleware(); - IAVSDirectory avsDirectory = _readAvsDirectory(); - OperatorConfig memory config = _readConfig("config/holesky/operators/eigenlayer/registerIntoBoltAVS.json"); - - console.log("Registering EigenLayer operator"); - console.log("Operator address:", operator); - console.log("Operator RPC:", config.rpc); - - bytes32 digest = avsDirectory.calculateOperatorAVSRegistrationDigestHash({ - operator: operator, - avs: address(middleware), - salt: config.salt, - expiry: config.expiry - }); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(operatorSk, digest); - bytes memory rawSignature = abi.encodePacked(r, s, v); - - ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = - ISignatureUtils.SignatureWithSaltAndExpiry(rawSignature, config.salt, config.expiry); - - vm.startBroadcast(operatorSk); - - middleware.registerOperator(config.rpc, operatorSignature); - console.log("Successfully registered EigenLayer operator"); - - vm.stopBroadcast(); - } - - function S03_checkOperatorRegistration() public view { - address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); - console.log("Checking operator registration for address", operatorAddress); - - IBoltManagerV2 boltManager = _readBoltManager(); - bool isRegistered = boltManager.isOperator(operatorAddress); - console.log("Operator is registered:", isRegistered); - require(isRegistered, "Operator is not registered"); - - BoltEigenLayerMiddlewareV2 middleware = _readMiddleware(); - (address[] memory tokens, uint256[] memory amounts) = middleware.getOperatorCollaterals(operatorAddress); - - for (uint256 i; i < tokens.length; ++i) { - if (amounts[i] > 0) { - console.log("Collateral found:", tokens[i], "- amount:", amounts[i]); - } - } - } - - function _readMiddleware() public view returns (BoltEigenLayerMiddlewareV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return BoltEigenLayerMiddlewareV2(vm.parseJsonAddress(json, ".eigenLayer.middleware")); - } - - function _readAvsDirectory() public view returns (IAVSDirectory) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IAVSDirectory(vm.parseJsonAddress(json, ".eigenLayer.avsDirectory")); - } - - function _readDelegationManager() public view returns (IDelegationManager) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IDelegationManager(vm.parseJsonAddress(json, ".eigenLayer.delegationManager")); - } - - function _readStrategyManager() public view returns (IStrategyManager) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IStrategyManager(vm.parseJsonAddress(json, ".eigenLayer.strategyManager")); - } - - function _readBoltManager() public view returns (IBoltManagerV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IBoltManagerV2(vm.parseJsonAddress(json, ".bolt.manager")); - } - - function _readConfig( - string memory path - ) public view returns (OperatorConfig memory) { - string memory json = vm.readFile(path); - - bytes32 salt = bytes32(0); - uint256 expiry = UINT256_MAX; - - try vm.parseJsonBytes32(json, ".salt") returns (bytes32 val) { - salt = val; - } catch { - console.log("No salt found in config, using 0"); - } - - try vm.parseJsonUint(json, ".expiry") returns (uint256 val) { - expiry = val; - } catch { - console.log("No expiry found in config, using UINT256_MAX"); - } - - return OperatorConfig({rpc: vm.parseJsonString(json, ".rpc"), salt: salt, expiry: expiry}); - } -} diff --git a/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol b/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol deleted file mode 100644 index 0bcea2005..000000000 --- a/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {Script, console} from "forge-std/Script.sol"; - -import {BoltSymbioticMiddlewareV2} from "../../../src/contracts/BoltSymbioticMiddlewareV2.sol"; -import {IBoltManagerV2} from "../../../src/interfaces/IBoltManagerV2.sol"; - -import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; -import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; - -contract RegisterSymbioticOperator is Script { - struct Config { - BoltSymbioticMiddlewareV2 symbioticMiddleware; - IOptInService symbioticNetworkOptInService; - address symbioticNetwork; - } - - function S01_registerIntoBolt() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - string memory rpc = vm.envString("OPERATOR_RPC"); - - address operator = vm.addr(operatorSk); - - Config memory config = _readConfig(); - - console.log("Registering Symbiotic operator into Bolt"); - console.log("Operator address:", operator); - - // First, make sure the operator is opted into the network - require( - config.symbioticNetworkOptInService.isOptedIn(operator, config.symbioticNetwork), - "Operator must be opted in into Bolt Network" - ); - - console.log("Operator RPC:", rpc); - - vm.startBroadcast(operatorSk); - config.symbioticMiddleware.registerOperator(rpc); - console.log("Successfully registered Symbiotic operator"); - - vm.stopBroadcast(); - - (address[] memory tokens, uint256[] memory amounts) = - config.symbioticMiddleware.getOperatorCollaterals(operator); - - console.log("Operator collateral:"); - for (uint256 i; i < tokens.length; ++i) { - console.log("Collateral:", tokens[i], "Amount:", amounts[i]); - } - } - - function S02_checkOperatorRegistration() public view { - address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); - console.log("Checking operator registration for address", operatorAddress); - - IBoltManagerV2 boltManager = _readBoltManager(); - bool isRegistered = boltManager.isOperator(operatorAddress); - - console.log("Operator is registered:", isRegistered); - require(isRegistered, "Operator is not registered"); - } - - function _readConfig() public view returns (Config memory) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return Config({ - symbioticNetwork: vm.parseJsonAddress(json, ".symbiotic.network"), - symbioticMiddleware: BoltSymbioticMiddlewareV2(vm.parseJsonAddress(json, ".symbiotic.middleware")), - symbioticNetworkOptInService: IOptInService(vm.parseJsonAddress(json, ".symbiotic.networkOptInService")) - }); - } - - function _readBoltManager() public view returns (IBoltManagerV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IBoltManagerV2(vm.parseJsonAddress(json, ".bolt.manager")); - } -} diff --git a/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol b/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol deleted file mode 100644 index cdd69ff2d..000000000 --- a/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {IBoltValidatorsV2} from "../../../src/interfaces/IBoltValidatorsV2.sol"; -import {BLS12381} from "../../../src/lib/bls/BLS12381.sol"; - -import {Script, console} from "forge-std/Script.sol"; - -/// @notice Script to register Ethereum validators to Bolt -/// @dev this script reads from the config file in /config/holesky/register_validators.json -contract RegisterValidators is Script { - using BLS12381 for BLS12381.G1Point; - - struct RegisterValidatorsConfig { - uint32 maxCommittedGasLimit; - address authorizedOperator; - // Note: for Unsafe registration (aka without BLS verification precompile) - // we use compressed pubkey hashes on-chain instead of decompressed points. - // BLS12381.G1Point[] pubkeys; - bytes20[] pubkeys; - } - - function run() public { - address controller = msg.sender; - - console.log("Registering validators to Bolt"); - console.log("Controller address: ", controller); - - IBoltValidatorsV2 validators = _readValidators(); - RegisterValidatorsConfig memory config = _parseConfig(); - - vm.startBroadcast(controller); - validators.batchRegisterValidatorsUnsafe(config.pubkeys, config.maxCommittedGasLimit, config.authorizedOperator); - vm.stopBroadcast(); - - console.log("Validators registered successfully"); - } - - function _readValidators() public view returns (IBoltValidatorsV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IBoltValidatorsV2(vm.parseJsonAddress(json, ".bolt.validators")); - } - - function _parseConfig() public view returns (RegisterValidatorsConfig memory config) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/validators.json"); - string memory json = vm.readFile(path); - - config.authorizedOperator = vm.parseJsonAddress(json, ".authorizedOperator"); - config.maxCommittedGasLimit = uint32(vm.parseJsonUint(json, ".maxCommittedGasLimit")); - console.log("Max committed gas limit:", config.maxCommittedGasLimit); - - string[] memory pubkeysRaw = vm.parseJsonStringArray(json, ".pubkeys"); - - // NOTE: for Unsafe registration (aka without BLS verification precompile) - // we use compressed pubkey hashes on-chain instead of decompressed points. - bytes20[] memory pubkeys = new bytes20[](pubkeysRaw.length); - for (uint256 i = 0; i < pubkeysRaw.length; i++) { - bytes memory pubkeyBytes = vm.parseBytes(pubkeysRaw[i]); - require(pubkeyBytes.length == 48, "Invalid pubkey length"); - - // compute the pubkey hash: - // 1. create a 64 byte buffer - // 2. copy the pubkey bytes to the rightmost 48 bytes of the buffer - // 3. hash the buffer - // 4. take the 20 leftmost bytes of the hash as the pubkey hash - bytes memory buffer = new bytes(64); - for (uint256 j = 0; j < 48; j++) { - buffer[j + 16] = pubkeyBytes[j]; - } - bytes20 pubkeyHash = bytes20(keccak256(buffer)); - - pubkeys[i] = pubkeyHash; - console.log("Registering pubkey hash:", vm.toString(abi.encodePacked(pubkeyHash))); - } - - // BLS12381.G1Point[] memory pubkeys = new BLS12381.G1Point[](pubkeysRaw.length); - // for (uint256 i = 0; i < pubkeysRaw.length; i++) { - // string memory pubkey = pubkeysRaw[i]; - - // string[] memory convertCmd = new string[](2); - // convertCmd[0] = "./script/pubkey_to_g1_wrapper.sh"; - // convertCmd[1] = pubkey; - - // bytes memory output = vm.ffi(convertCmd); - // string memory outputStr = string(output); - // string[] memory array = vm.split(outputStr, ","); - - // uint256[2] memory x = _bytesToParts(vm.parseBytes(array[0])); - // uint256[2] memory y = _bytesToParts(vm.parseBytes(array[1])); - - // pubkeys[i] = BLS12381.G1Point(x, y); - - // console.log("Registering pubkey:", vm.toString(abi.encodePacked(pubkeys[i].compress()))); - // } - - config.pubkeys = pubkeys; - } - - function _bytesToParts( - bytes memory data - ) public pure returns (uint256[2] memory out) { - require(data.length == 48, "Invalid data length"); - - uint256 value1; - uint256 value2; - - // Load the first 32 bytes into value1 - assembly { - value1 := mload(add(data, 32)) - } - value1 = value1 >> 128; // Clear unwanted upper bits - - // Load the next 16 bytes into value2 - assembly { - value2 := mload(add(data, 48)) - } - // value2 = value2 >> 128; - - out[0] = value1; - out[1] = value2; - } -} diff --git a/testnets/holesky/README.md b/testnets/holesky/README.md index 1d273a116..37d1db1bb 100644 --- a/testnets/holesky/README.md +++ b/testnets/holesky/README.md @@ -6,30 +6,33 @@ This document provides instructions for running Bolt on the Holesky testnet. -- [Prerequisites](#prerequisites) -- [On-Chain Registration](#on-chain-registration) - - [Validator Registration](#validator-registration) - - [Registration Steps](#registration-steps) - - [Bolt Network Entrypoint](#bolt-network-entrypoint) - - [Operator Registration](#operator-registration) - - [Symbiotic Registration Steps](#symbiotic-registration-steps) - - [EigenLayer Registration Steps](#eigenlayer-registration-steps) -- [Off-Chain Setup](#off-chain-setup) - - [Docker Mode (recommended)](#docker-mode-recommended) - - [Commit-Boost Mode](#commit-boost-mode) - - [Native Mode (advanced)](#native-mode-advanced) - - [Building and running the MEV-Boost fork binary](#building-and-running-the-mev-boost-fork-binary) - - [Building and running the Bolt sidecar binary](#building-and-running-the-bolt-sidecar-binary) - - [Configuration file](#configuration-file) - - [Observability](#observability) -- [Reference](#reference) - - [Command-line options](#command-line-options) - - [Delegations and signing options for Native and Docker Compose Mode](#delegations-and-signing-options-for-native-and-docker-compose-mode) - - [`bolt` CLI](#bolt-cli) - - [Installation and usage](#installation-and-usage) - - [Using a private key directly](#using-a-private-key-directly) - - [Using a ERC-2335 Keystore](#using-a-erc-2335-keystore) - - [Avoid restarting the beacon node](#avoid-restarting-the-beacon-node) +* [Prerequisites](#prerequisites) +* [On-Chain Registration](#on-chain-registration) + * [Validator Registration](#validator-registration) + * [Registration Steps](#registration-steps) + * [Bolt Network Entrypoint](#bolt-network-entrypoint) + * [Operator Registration](#operator-registration) + * [Symbiotic Registration Steps](#symbiotic-registration-steps) + * [EigenLayer Registration Steps](#eigenlayer-registration-steps) +* [Off-Chain Setup](#off-chain-setup) + * [Docker Mode (recommended)](#docker-mode-recommended) + * [Commit-Boost Mode](#commit-boost-mode) + * [Native Mode (advanced)](#native-mode-advanced) + * [Building and running the MEV-Boost fork binary](#building-and-running-the-mev-boost-fork-binary) + * [Building and running the Bolt sidecar binary](#building-and-running-the-bolt-sidecar-binary) + * [Configuration file](#configuration-file) + * [Observability](#observability) + * [Firewall Configuration](#firewall-configuration) +* [Reference](#reference) + * [Supported RPC nodes](#supported-rpc-nodes) + * [Supported Relays](#supported-relays) + * [Command-line options](#command-line-options) + * [Delegations and signing options for Native and Docker Compose Mode](#delegations-and-signing-options-for-native-and-docker-compose-mode) + * [`bolt` CLI](#bolt-cli) + * [Installation and usage](#installation-and-usage) + * [Using a private key directly](#using-a-private-key-directly) + * [Using a ERC-2335 Keystore](#using-a-erc-2335-keystore) + * [Avoid restarting the beacon node](#avoid-restarting-the-beacon-node) @@ -135,22 +138,48 @@ protocols. **Prerequisites** -- Install the Foundry toolkit: +- Make sure you have Rust installed on your machine. Follow the instructions + reported in the [official website](https://www.rust-lang.org/tools/install). -```bash -curl -L https://foundry.paradigm.xyz | bash -source $HOME/.bashrc -foundryup -``` +- Clone the Bolt repo and install the `bolt` CLI -- Clone the Bolt repo and navigate to the [contracts](https://github.com/chainbound/bolt/tree/unstable/bolt-contracts) directory: + ```bash + git clone https://github.com/chainbound/bolt + cd bolt-cli + cargo install --force --path . + ``` -```bash -git clone https://github.com/chainbound/bolt -cd bolt-contracts -forge install +The command above will install the `bolt` CLI in your system, which is a useful tool to +manage your validators and operators in the bolt Protocol. You can check if +it's installed by running `bolt --help`. + +```text +`bolt` is a CLI tool to interact with bolt Protocol ✨ + +Usage: bolt + +Commands: + delegate Generate BLS delegation or revocation messages + pubkeys Output a list of pubkeys in JSON format + send Send a preconfirmation request to a bolt proposer + validators Handle validators in the bolt network + operators Handle operators in the bolt network + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version ``` +> [!NOTE] +> All the `bolt` commands can be simulated on a Holesky fork using Anvil with +> the following command: +> +> `anvil --fork-url https://holesky.drpc.org --port 8545` +> +> In order to use this local fork, replace the `--rpc-url` flag (`$RPC_URL` env) +> with `http://localhost:8545` in all `bolt` commands below. + ## Validator Registration The [`BoltValidators`](../../bolt-contracts/src/contracts/BoltValidatorsV1.sol) contract is the only @@ -178,40 +207,32 @@ validator or change any preferences. ### Registration Steps -> [!NOTE] -> All of these scripts can be simulated on a Holesky fork using Anvil with the -> following command: -> -> `anvil --fork-url https://holesky.drpc.org --port 8545` -> -> In order to use this local fork, replace `$HOLESKY_RPC` with `localhost:8545` in -> all of the `forge` commands below. - -To register your validators, we provide the following Foundry script: -[`RegisterValidators.s.sol`](../../bolt-contracts/script/holesky/validators/RegisterValidators.s.sol). -Note that in order to run these scripts, you must be in the `bolt-contracts` -directory. +To register your validators, you can use the `bolt` CLI. First, look at the +options available for the `validators register` command: -- First, configure - [`bolt-contracts/config/holesky/validators.json`](../../bolt-contracts/config/holesky/validators.json) - to your requirements. Note that both `maxCommittedGasLimit` and - `authorizedOperator` must reflect the values you'll specify in later steps, during - the configuration of the sidecar. `pubkeys` should be configured with all of the - validator public keys that you wish to register. - -- Next up, decide on a controller account and save the key in an environment - variable: `export CONTROLLER_KEY=0x...`. This controller key will be used to run - the script and will mark the corresponding account as the [controller - account](https://github.com/chainbound/bolt/blob/06bdd8e75d759d91f6178ad73f962b1f4ad43fd8/bolt-contracts/src/interfaces/IBoltValidatorsV1.sol#L18-L19) - for these validators. - -- Finally, run the script: +```text +Usage: bolt validators register [OPTIONS] --rpc-url --max-committed-gas-limit --authorized-operator --admin-private-key -```bash -forge script script/holesky/validators/RegisterValidators.s.sol -vvvv --rpc-url $HOLESKY_RPC --private-key $CONTROLLER_KEY --broadcast +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --max-committed-gas-limit + The max gas limit the validator is willing to reserve to commitments [env: MAX_COMMITTED_GAS_LIMIT=] + --authorized-operator + The authorized operator for the validator [env: AUTHORIZED_OPERATOR=] + --pubkeys-path + The path to the JSON pubkeys file, containing an array of BLS public keys [env: PUBKEYS_PATH=] [default: pubkeys.json] + --admin-private-key + The private key to sign the transactions with [env: ADMIN_PRIVATE_KEY=] + -h, --help + Print help ``` -If the script executed succesfully, your validators were registered. +To generate the JSON file containing the pubkeys, you can use the `bolt pubkeys` +command. See `bolt pubkeys --help` for more info. + +Fill the required options and run the script. If the script executed +succesfully, your validators were registered. ## Bolt Network Entrypoint @@ -240,12 +261,6 @@ private key will be used to sign commitments on the corresponding validators' sidecars. However, we need a way to logically connect validators to an on-chain address associated with some stake, which is what the operator abstraction takes care of. -**In the next sections we assume you have saved the private key corresponding to -the operator address in `$OPERATOR_SK`.** This private key will be read by the -Forge scripts for registering operators and needs to be set correctly. You also -have to invoke the scripts from the [`bolt-contracts`](../../bolt-contracts) -directory. - ### Symbiotic Registration Steps As an operator, you will need to opt-in to the Bolt Network and any Vault that @@ -277,30 +292,41 @@ The opt-in process requires the following steps: **Internal Steps** After having deposited collateral into a vault you need to register into -Bolt as a Symbiotic operator. We've provided a script to facilitate the -procedure. If you want to use it, please follow these steps: +Bolt as a Symbiotic operator. You can do that using the `bolt` CLI. -1. set the operator private key to the `OPERATOR_SK` environment variable; -2. set the operator RPC URL which supports the Commitments API to the - `OPERATOR_RPC` environment variable; -3. run the following Forge script from the `bolt-contracts` directory: +First, read the requirements for the `bolt operator symbiotic register` command: - ```bash - forge script script/holesky/operators/RegisterSymbioticOperator.s.sol \ - --sig "S01_registerIntoBolt" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast - ``` +```text +Register into the bolt manager contract as a Symbiotic operator -To check if your operator is correctly registered, set the operator address -in the `OPERATOR_ADDRESS` environment variable and run the following script: +Usage: bolt operators symbiotic register --rpc-url --operator-private-key --operator-rpc -```bash -forge script script/holesky/operators/RegisterSymbioticOperator.s.sol \ - --sig "S02_checkOperatorRegistration" \ - --rpc-url $HOLESKY_RPC \ - -vvvv +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --operator-rpc + The URL of the operator RPC [env: OPERATOR_RPC=] + -h, --help + Print help +``` + +Fill the required options and run the script. If the script executed +successfully, your validators were registered. + +To check your operator status, you can use the `bolt operator +symbiotic status` command: + +```text +Check the status of a Symbiotic operator + +Usage: bolt operators symbiotic status --rpc-url --address
+ +Options: + --rpc-url The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --address
The address of the operator to check [env: OPERATOR_ADDRESS=] + -h, --help Print help ``` ### EigenLayer Registration Steps @@ -326,79 +352,90 @@ This will add the deposit into the collateral of the operator so that Bolt can read it. Note that you need to deposit a minimum of `1 ether` of the strategies underlying token in order to opt in. -We've provided a script to facilitate the procedure. If you want to use it, -please set the operator private key to an `OPERATOR_SK` environment variable. +You can deposit into a strategy by using the following `bolt operators +eigenlayer deposit` command: -First, you need to first configure the deposit details in this JSON -file: +```text +Step 1: Deposit into a strategy -```bash -$EDITOR ./config/holesky/operators/eigenlayer/depositIntoStrategy.json -``` +Usage: bolt operators eigenlayer deposit --rpc-url --operator-private-key --strategy --amount -Note that the amount is in ether (so for 1 ether, specify `1` instead of 1e18). +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --strategy + The name of the strategy to deposit into [env: EIGENLAYER_STRATEGY=] [possible values: st-eth, r-eth, w-eth, cb-eth, m-eth] + --amount + The amount to deposit into the strategy, in ETH [env: EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT=] + -h, --help + Print help +``` -Then you can run the following Forge script: +Note that the amount is in ETH, so if you want to deposit `1 ether` you need to +provide `--amount 1`. -```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S01_depositIntoStrategy()" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast -``` +Fill the required options and run the script. If the script executed +successfully, you have deposited into the strategy. **Internal Steps** After having deposited collateral into a strategy you need to register into the -Bolt AVS. We've provided a script to facilitate the procedure. If you want to -use it, please set follow these steps: +Bolt AVS. You can use the `bolt operators eigenlayer register` command for it: -1. configure the operator details in this JSON file +```text +Step 2: Register into the bolt AVS - ```bash - $EDITOR ./config/holesky/operators/eigenlayer/registerIntoBoltAVS.json - ``` +Usage: bolt operators eigenlayer register --rpc-url --operator-private-key --operator-rpc --salt --expiry - In there you'll need to set the the following fields: +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --operator-rpc + The URL of the operator RPC [env: OPERATOR_RPC=] + --salt + The salt for the operator signature [env: OPERATOR_SIGNATURE_SALT=] + --expiry + The expiry timestamp for the operator signature [env: OPERATOR_SIGNATURE_EXPIRY=] + -h, --help + Print help +``` - - `rpc` -- the RPC URL of your operator which supports the Commitments API - - `salt` -- an unique 32 bytes value to avoid replay attacks. To generate it on - both Linux and MacOS you can run: +A note on the `--salt` and `--expiry` parameters: - ```bash - echo -n "0x"; head -c 32 /dev/urandom | hexdump -e '32/1 "%02x" "\n"' - ``` +- `salt` -- an unique 32 bytes value to avoid replay attacks. To generate it on + both Linux and MacOS you can run: - - `expiry` -- the timestamp of the signature expiry in seconds. To generate it - on both Linux and MacOS run the following command, replacing - `` with the desired timestamp: + ```bash + echo -n "0x"; head -c 32 /dev/urandom | hexdump -e '32/1 "%02x" "\n"' + ``` - ```bash - echo -n "0x"; printf "%064x\n" - ``` +- `expiry` -- the timestamp of the signature expiry in seconds. To generate it + on both Linux and MacOS run the following command, replacing + `` with the desired timestamp: -2. set the operator private key to an `OPERATOR_SK` environment - variable; -3. run the following Forge script from the `bolt-contracts` - directory: + ```bash + echo -n "0x"; printf "%064x\n" + ``` -```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S02_registerIntoBoltAVS" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast -``` +Once you have the required values, fill the options and run the script. If the +command executed successfully, your operator were registered into bolt. -To check if your operator is correctly registered, set the operator address -in the `OPERATOR_ADDRESS` environment variable and run the following script: +To check if the status of your operator, you can use the `bolt operators +eigenlayer status` command: ```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S03_checkOperatorRegistration" \ - --rpc-url $HOLESKY_RPC \ - -vvvv +Step 3: Check your operation registration in bolt + +Usage: bolt operators eigenlayer status --rpc-url --address
+ +Options: + --rpc-url The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --address
The address of the operator to check [env: OPERATOR_ADDRESS=] + -h, --help Print help ``` # Off-Chain Setup